Skip to content

Commit 53a0c70

Browse files
Add generic option type
1 parent 56050e3 commit 53a0c70

29 files changed

+167
-146
lines changed

src/behaviors/async.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { optionType } from '../propTypes';
1818
import { getDisplayName, isFunction } from '../utils';
1919

2020
import { TypeaheadComponentProps } from '../components/Typeahead';
21-
import type { Option } from '../types';
21+
import type { OptionType } from '../types';
2222

2323
const propTypes = {
2424
/**
@@ -57,7 +57,7 @@ const propTypes = {
5757
useCache: PropTypes.bool,
5858
};
5959

60-
export interface UseAsyncProps extends TypeaheadComponentProps {
60+
export interface UseAsyncProps<Option extends OptionType> extends TypeaheadComponentProps<Option> {
6161
delay?: number;
6262
isLoading: boolean;
6363
onSearch: (query: string) => void;
@@ -66,7 +66,7 @@ export interface UseAsyncProps extends TypeaheadComponentProps {
6666
useCache?: boolean;
6767
}
6868

69-
type Cache = Record<string, Option[]>;
69+
type Cache = Record<string, OptionType[]>;
7070

7171
interface DebouncedFunction extends Function {
7272
cancel(): void;
@@ -80,7 +80,7 @@ interface DebouncedFunction extends Function {
8080
* - Optional query caching
8181
* - Search prompt and empty results behaviors
8282
*/
83-
export function useAsync(props: UseAsyncProps) {
83+
export function useAsync<Option extends OptionType>(props: UseAsyncProps<Option>) {
8484
const {
8585
allowNew,
8686
delay = 200,

src/behaviors/item.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { useTypeaheadContext } from '../core/Context';
1414
import { getDisplayName, getMenuItemId, preventInputBlur } from '../utils';
1515

1616
import { optionType } from '../propTypes';
17-
import { Option } from '../types';
17+
import { OptionType } from '../types';
1818

1919
const propTypes = {
2020
option: optionType.isRequired,
@@ -23,7 +23,7 @@ const propTypes = {
2323

2424
export interface UseItemProps<T> extends HTMLProps<T> {
2525
onClick?: MouseEventHandler<T>;
26-
option: Option;
26+
option: OptionType;
2727
position: number;
2828
}
2929

src/behaviors/token.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ import { useRootClose } from 'react-overlays';
1414
import { getDisplayName, isFunction } from '../utils';
1515

1616
import { optionType } from '../propTypes';
17-
import { Option, OptionHandler, RefElement } from '../types';
17+
import { OptionType, OptionHandler, RefElement } from '../types';
1818

19-
export interface UseTokenProps<T> extends Omit<HTMLProps<T>, 'onBlur'> {
19+
export interface UseTokenProps<T, Option extends OptionType> extends Omit<HTMLProps<T>, 'onBlur'> {
2020
// `onBlur` is typed more generically because it's passed to `useRootClose`,
2121
// which passes a generic event to the callback.
2222
onBlur?: (event: Event) => void;
2323
onClick?: MouseEventHandler<T>;
2424
onFocus?: FocusEventHandler<T>;
25-
onRemove?: OptionHandler;
25+
onRemove?: OptionHandler<Option>;
2626
option: Option;
2727
}
2828

@@ -34,14 +34,14 @@ const propTypes = {
3434
option: optionType.isRequired,
3535
};
3636

37-
export function useToken<T extends HTMLElement>({
37+
export function useToken<T extends HTMLElement, Option extends OptionType>({
3838
onBlur,
3939
onClick,
4040
onFocus,
4141
onRemove,
4242
option,
4343
...props
44-
}: UseTokenProps<T>) {
44+
}: UseTokenProps<T, Option>) {
4545
const [active, setActive] = useState<boolean>(false);
4646
const [rootElement, attachRef] = useState<RefElement<T>>(null);
4747

src/components/MenuItem/MenuItem.stories.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@ import {
99
TypeaheadContext,
1010
TypeaheadContextType,
1111
} from '../../core/Context';
12+
import {OptionType} from "../../types";
1213

1314
export default {
1415
title: 'Components/MenuItem/MenuItem',
1516
component: MenuItem,
1617
} as Meta;
1718

1819
interface Args {
19-
context: Partial<TypeaheadContextType>;
20+
context: Partial<TypeaheadContextType<OptionType>>;
2021
props: MenuItemProps;
2122
}
2223

src/components/Token/Token.stories.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ import { Story, Meta } from '@storybook/react';
55

66
import Token, { TokenProps } from './Token';
77
import { noop } from '../../utils';
8+
import {OptionType} from "../../types";
89

910
export default {
1011
title: 'Components/Token',
1112
component: Token,
1213
} as Meta;
1314

14-
const Template: Story<TokenProps<HTMLElement>> = (args) => <Token {...args} />;
15+
const Template: Story<TokenProps<HTMLElement, OptionType>> = (args) => <Token {...args} />;
1516

1617
export const Interactive = Template.bind({});
1718
Interactive.args = {

src/components/Token/Token.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import ClearButton from '../ClearButton';
55

66
import { useToken, UseTokenProps } from '../../behaviors/token';
77
import { isFunction } from '../../utils';
8+
import {OptionType} from "../../types";
89

910
type HTMLElementProps = Omit<HTMLProps<HTMLDivElement>, 'onBlur' | 'ref'>;
1011

@@ -68,7 +69,7 @@ const StaticToken = ({
6869
return <div className={classnames}>{children}</div>;
6970
};
7071

71-
export interface TokenProps<T> extends UseTokenProps<T> {
72+
export interface TokenProps<T, Option extends OptionType> extends UseTokenProps<T, Option> {
7273
disabled?: boolean;
7374
readOnly?: boolean;
7475
}
@@ -77,12 +78,12 @@ export interface TokenProps<T> extends UseTokenProps<T> {
7778
* Individual token component, generally displayed within the
7879
* `TypeaheadInputMulti` component, but can also be rendered on its own.
7980
*/
80-
const Token = ({
81+
const Token = <Option extends OptionType>({
8182
children,
8283
option,
8384
readOnly,
8485
...props
85-
}: TokenProps<HTMLElement>) => {
86+
}: TokenProps<HTMLElement, Option>) => {
8687
const { ref, ...tokenProps } = useToken({ ...props, option });
8788
const child = <div className="rbt-token-label">{children}</div>;
8889

src/components/Typeahead/Typeahead.stories.tsx

+6-5
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import Hint from '../Hint';
99
import Menu from '../Menu';
1010
import MenuItem from '../MenuItem';
1111

12-
import options, { Option } from '../../tests/data';
12+
import options, { TestOption } from '../../tests/data';
1313
import { noop } from '../../tests/helpers';
14+
import {OptionType} from "../../types";
1415

1516
export default {
1617
title: 'Components/Typeahead',
@@ -53,7 +54,7 @@ const defaultProps = {
5354
positionFixed: true,
5455
};
5556

56-
const Template: Story<TypeaheadComponentProps> = (args) => (
57+
const Template: Story<TypeaheadComponentProps<TestOption>> = (args) => (
5758
<Typeahead {...args} />
5859
);
5960

@@ -122,7 +123,7 @@ CustomMenu.args = {
122123
renderMenu: (results, menuProps) => (
123124
<Menu {...menuProps}>
124125
{/* Use `slice` to avoid mutating the original array */}
125-
{(results as Option[])
126+
{(results as TestOption[])
126127
.slice()
127128
.reverse()
128129
.map((r, index) => (
@@ -134,7 +135,7 @@ CustomMenu.args = {
134135
),
135136
};
136137

137-
export const InputGrouping = (args: TypeaheadComponentProps) => (
138+
export const InputGrouping = <Option extends OptionType>(args: TypeaheadComponentProps<Option>) => (
138139
<div
139140
className={cx('input-group', {
140141
'input-group-sm': args.size === 'sm',
@@ -149,7 +150,7 @@ InputGrouping.args = {
149150
...defaultProps,
150151
};
151152

152-
export const Controlled = (args: TypeaheadComponentProps) => {
153+
export const Controlled = <Option extends OptionType>(args: TypeaheadComponentProps<Option>) => {
153154
const [selected, setSelected] = useState(args.selected || []);
154155

155156
return <Typeahead {...args} onChange={setSelected} selected={selected} />;

src/components/Typeahead/Typeahead.test.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ import {
2929
waitFor,
3030
} from '../../tests/helpers';
3131

32-
import states from '../../tests/data';
32+
import states, {TestOption} from '../../tests/data';
3333

3434
const ID = 'rbt-id';
3535

36-
const TestComponent = forwardRef<Typeahead, Partial<TypeaheadComponentProps>>(
36+
const TestComponent = forwardRef<Typeahead<TestOption>, Partial<TypeaheadComponentProps<TestOption>>>(
3737
(props, ref) => (
3838
<TypeaheadComponent
3939
id={ID}

src/components/Typeahead/Typeahead.tsx

+38-26
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
import { checkPropType, inputPropsType, sizeType } from '../../propTypes';
3333

3434
import {
35-
Option,
35+
OptionType,
3636
RefElement,
3737
RenderToken,
3838
RenderTokenProps,
@@ -42,23 +42,23 @@ import {
4242
TypeaheadManagerChildProps,
4343
} from '../../types';
4444

45-
export interface RenderMenuProps
45+
export interface RenderMenuProps<Option extends OptionType>
4646
extends Omit<
47-
TypeaheadMenuProps,
47+
TypeaheadMenuProps<Option>,
4848
'labelKey' | 'options' | 'renderMenuItemChildren' | 'text'
4949
> {
50-
renderMenuItemChildren?: RenderMenuItemChildren;
50+
renderMenuItemChildren?: RenderMenuItemChildren<Option>;
5151
}
5252

53-
export interface TypeaheadComponentProps extends Partial<TypeaheadProps> {
53+
export interface TypeaheadComponentProps<Option extends OptionType> extends Partial<TypeaheadProps<Option>> {
5454
align?: Align;
5555
className?: string;
5656
clearButton?: boolean;
5757
disabled?: boolean;
5858
dropup?: boolean;
5959
emptyLabel?: ReactNode;
6060
flip?: boolean;
61-
instanceRef?: Ref<Typeahead>;
61+
instanceRef?: Ref<Typeahead<Option>>;
6262
isInvalid?: boolean;
6363
isLoading?: boolean;
6464
isValid?: boolean;
@@ -70,15 +70,15 @@ export interface TypeaheadComponentProps extends Partial<TypeaheadProps> {
7070
positionFixed?: boolean;
7171
renderInput?: (
7272
inputProps: TypeaheadInputProps,
73-
props: TypeaheadManagerChildProps
73+
props: TypeaheadManagerChildProps<Option>
7474
) => JSX.Element;
7575
renderMenu?: (
7676
results: Option[],
77-
menuProps: RenderMenuProps,
78-
state: TypeaheadManagerChildProps
77+
menuProps: RenderMenuProps<Option>,
78+
state: TypeaheadManagerChildProps<Option>
7979
) => JSX.Element;
80-
renderMenuItemChildren?: RenderMenuItemChildren;
81-
renderToken?: RenderToken;
80+
renderMenuItemChildren?: RenderMenuItemChildren<Option>;
81+
renderToken?: RenderToken<Option>;
8282
size?: Size;
8383
style?: CSSProperties;
8484
}
@@ -127,10 +127,10 @@ const defaultProps = {
127127
isLoading: false,
128128
};
129129

130-
const defaultRenderMenu = (
130+
const defaultRenderMenu = <Option extends OptionType>(
131131
results: Option[],
132-
menuProps: RenderMenuProps,
133-
props: TypeaheadManagerChildProps
132+
menuProps: RenderMenuProps<Option>,
133+
props: TypeaheadManagerChildProps<Option>
134134
) => (
135135
<TypeaheadMenu
136136
{...menuProps}
@@ -140,9 +140,9 @@ const defaultRenderMenu = (
140140
/>
141141
);
142142

143-
const defaultRenderToken = (
143+
const defaultRenderToken = <Option extends OptionType>(
144144
option: Option,
145-
props: RenderTokenProps,
145+
props: RenderTokenProps<Option>,
146146
idx: number
147147
) => (
148148
<Token
@@ -160,9 +160,9 @@ const overlayPropKeys = [
160160
'dropup',
161161
'flip',
162162
'positionFixed',
163-
] as (keyof TypeaheadComponentProps)[];
163+
] as (keyof TypeaheadComponentProps<OptionType>)[];
164164

165-
function getOverlayProps(props: TypeaheadComponentProps) {
165+
function getOverlayProps<Option extends OptionType>(props: TypeaheadComponentProps<Option>) {
166166
return pick(props, overlayPropKeys);
167167
}
168168

@@ -178,7 +178,7 @@ const RootClose = ({ children, onRootClose, ...props }: RootCloseProps) => {
178178
return children(attachRef);
179179
};
180180

181-
class TypeaheadComponent extends React.Component<TypeaheadComponentProps> {
181+
class TypeaheadComponent<Option extends OptionType> extends React.Component<TypeaheadComponentProps<Option>> {
182182
static propTypes = propTypes;
183183
static defaultProps = defaultProps;
184184

@@ -190,7 +190,7 @@ class TypeaheadComponent extends React.Component<TypeaheadComponentProps> {
190190

191191
return (
192192
<Typeahead {...this.props} options={options} ref={instanceRef}>
193-
{(props: TypeaheadManagerChildProps) => {
193+
{(props: TypeaheadManagerChildProps<Option>) => {
194194
const { hideMenu, isMenuShown, results } = props;
195195
const auxContent = this._renderAux(props);
196196

@@ -238,7 +238,7 @@ class TypeaheadComponent extends React.Component<TypeaheadComponentProps> {
238238

239239
_renderInput = (
240240
inputProps: TypeaheadInputProps,
241-
props: TypeaheadManagerChildProps
241+
props: TypeaheadManagerChildProps<Option>
242242
) => {
243243
const { isInvalid, isValid, multiple, renderInput, renderToken, size } =
244244
this.props;
@@ -279,7 +279,7 @@ class TypeaheadComponent extends React.Component<TypeaheadComponentProps> {
279279
_renderMenu = (
280280
results: Option[],
281281
menuProps: OverlayRenderProps,
282-
props: TypeaheadManagerChildProps
282+
props: TypeaheadManagerChildProps<Option>
283283
) => {
284284
const {
285285
emptyLabel,
@@ -306,7 +306,7 @@ class TypeaheadComponent extends React.Component<TypeaheadComponentProps> {
306306
);
307307
};
308308

309-
_renderAux = ({ onClear, selected }: TypeaheadManagerChildProps) => {
309+
_renderAux = ({ onClear, selected }: TypeaheadManagerChildProps<Option>) => {
310310
const { clearButton, disabled, isLoading, size } = this.props;
311311

312312
let content;
@@ -331,6 +331,18 @@ class TypeaheadComponent extends React.Component<TypeaheadComponentProps> {
331331
};
332332
}
333333

334-
export default forwardRef<Typeahead, TypeaheadComponentProps>((props, ref) => (
335-
<TypeaheadComponent {...props} instanceRef={ref} />
336-
));
334+
const TypeaheadComponentInner = <Option extends OptionType>(props: TypeaheadComponentProps<Option>, ref: React.ForwardedRef<Typeahead<Option>>) => <TypeaheadComponent {...props} instanceRef={ref} />
335+
336+
const TypeaheadComponentWithRef = forwardRef(TypeaheadComponentInner);
337+
338+
type TypeaheadComponentWithRefProps<Option extends OptionType> = TypeaheadComponentProps<Option> & {
339+
ref?: React.Ref<Typeahead<Option>>;
340+
};
341+
342+
export default function TypeaheadComp<Option extends OptionType>({
343+
ref,
344+
...props
345+
}: TypeaheadComponentWithRefProps<Option>) {
346+
// @ts-ignore
347+
return <TypeaheadComponentWithRef ref={ref} {...props} />;
348+
}

src/components/TypeaheadInputMulti/TypeaheadInputMulti.stories.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import TypeaheadInputMulti, {
1111
import options from '../../tests/data';
1212
import { HintProvider, noop } from '../../tests/helpers';
1313
import type { Size } from '../../types';
14+
import {OptionType} from "../../types";
1415

1516
export default {
1617
title: 'Components/TypeaheadInputMulti',
@@ -28,7 +29,7 @@ const defaultProps = {
2829
selected,
2930
};
3031

31-
interface Args extends TypeaheadInputMultiProps {
32+
interface Args<Option extends OptionType> extends TypeaheadInputMultiProps<Option> {
3233
hintText?: string;
3334
isValid?: boolean;
3435
isInvalid?: boolean;

0 commit comments

Comments
 (0)