Skip to content

Commit 4ece5e0

Browse files
Add generic option type
1 parent 79f74b9 commit 4ece5e0

22 files changed

+104
-71
lines changed

src/behaviors/async.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export interface UseAsyncProps<Option extends OptionType> extends TypeaheadCompo
6666
useCache?: boolean;
6767
}
6868

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

7171
interface DebouncedFunction extends Function {
7272
cancel(): void;
@@ -96,7 +96,7 @@ export function useAsync<Option extends OptionType>(props: UseAsyncProps<Option>
9696
...otherProps
9797
} = props;
9898

99-
const cacheRef = useRef<Cache>({});
99+
const cacheRef = useRef<Cache<Option>>({});
100100
const handleSearchDebouncedRef = useRef<DebouncedFunction | null>(null);
101101
const queryRef = useRef<string>(props.defaultInputValue || '');
102102

@@ -178,10 +178,10 @@ export function useAsync<Option extends OptionType>(props: UseAsyncProps<Option>
178178
};
179179
}
180180

181-
export function withAsync<T extends UseAsyncProps = UseAsyncProps>(
181+
export function withAsync<Option extends OptionType, T extends UseAsyncProps<Option> = UseAsyncProps<Option>>(
182182
Component: ComponentType<T>
183183
) {
184-
const AsyncTypeahead = forwardRef<Typeahead, T>((props, ref) => (
184+
const AsyncTypeahead = forwardRef<Typeahead<Option>, T>((props, ref) => (
185185
<Component {...props} {...useAsync(props)} ref={ref} />
186186
));
187187

src/behaviors/item.tsx

+6-6
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,19 @@ const propTypes = {
2121
position: PropTypes.number,
2222
};
2323

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

30-
export function useItem<T extends HTMLElement>({
30+
export function useItem<T extends HTMLElement, Option extends OptionType>({
3131
label,
3232
onClick,
3333
option,
3434
position,
3535
...props
36-
}: UseItemProps<T>) {
36+
}: UseItemProps<T, Option>) {
3737
const {
3838
activeIndex,
3939
id,
@@ -42,7 +42,7 @@ export function useItem<T extends HTMLElement>({
4242
onInitialItemChange,
4343
onMenuItemClick,
4444
setItem,
45-
} = useTypeaheadContext();
45+
} = useTypeaheadContext<Option>();
4646

4747
const itemRef = useRef<T>(null);
4848

@@ -96,7 +96,7 @@ export function useItem<T extends HTMLElement>({
9696
}
9797

9898
/* istanbul ignore next */
99-
export function withItem<T extends UseItemProps<HTMLElement>>(
99+
export function withItem<Option extends OptionType, T extends UseItemProps<HTMLElement, Option>>(
100100
Component: ComponentType<T>
101101
) {
102102
const WrappedMenuItem = (props: T) => (

src/behaviors/token.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export function useToken<T extends HTMLElement, Option extends OptionType>({
8989
}
9090

9191
/* istanbul ignore next */
92-
export function withToken<T extends UseTokenProps<HTMLElement>>(
92+
export function withToken<Option extends OptionType, T extends UseTokenProps<HTMLElement, Option>>(
9393
Component: ComponentType<T>
9494
) {
9595
const displayName = `withToken(${getDisplayName(Component)})`;

src/components/MenuItem/MenuItem.stories.tsx

+7-6
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,19 @@ import { Story, Meta } from '@storybook/react';
55

66
import MenuItem, { MenuItemProps } from './MenuItem';
77
import {
8+
createTypeaheadContext,
89
defaultContext,
9-
TypeaheadContext,
1010
TypeaheadContextType,
1111
} from '../../core/Context';
12-
import {OptionType} from "../../types";
12+
import {TestOption} from "../../tests/data";
1313

1414
export default {
1515
title: 'Components/MenuItem/MenuItem',
1616
component: MenuItem,
1717
} as Meta;
1818

1919
interface Args {
20-
context: Partial<TypeaheadContextType<OptionType>>;
20+
context: Partial<TypeaheadContextType<TestOption>>;
2121
props: MenuItemProps;
2222
}
2323

@@ -26,11 +26,12 @@ const value = {
2626
id: 'test-id',
2727
};
2828

29-
const Template: Story<Args> = ({ context, props }) => (
30-
<TypeaheadContext.Provider value={{ ...value, ...context }}>
29+
const Template: Story<Args> = ({ context, props }) => {
30+
const TypeaheadContext = createTypeaheadContext<TestOption>()
31+
return <TypeaheadContext.Provider value={{...value, ...context}}>
3132
<MenuItem {...props} />
3233
</TypeaheadContext.Provider>
33-
);
34+
};
3435

3536
export const Default = Template.bind({});
3637
Default.args = {

src/components/MenuItem/MenuItem.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import cx from 'classnames';
22
import React, { forwardRef, HTMLAttributes, MouseEvent } from 'react';
33

44
import { useItem, UseItemProps } from '../../behaviors/item';
5+
import {OptionType} from "../../types";
56

67
export interface BaseMenuItemProps extends HTMLAttributes<HTMLAnchorElement> {
78
active?: boolean;
@@ -27,8 +28,8 @@ export const BaseMenuItem = forwardRef<HTMLAnchorElement, BaseMenuItemProps>(
2728
}
2829
);
2930

30-
export type MenuItemProps = UseItemProps<HTMLAnchorElement>;
31+
export type MenuItemProps<Option extends OptionType> = UseItemProps<HTMLAnchorElement, Option>;
3132

32-
export default function MenuItem(props: MenuItemProps) {
33+
export default function MenuItem<Option extends OptionType>(props: MenuItemProps<Option>) {
3334
return <BaseMenuItem {...useItem(props)} />;
3435
}

src/components/Token/Token.stories.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,14 +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";
8+
import {TestOption} from "../../tests/data";
99

1010
export default {
1111
title: 'Components/Token',
1212
component: Token,
1313
} as Meta;
1414

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

1717
export const Interactive = Template.bind({});
1818
Interactive.args = {

src/components/TypeaheadInputMulti/TypeaheadInputMulti.stories.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import TypeaheadInputMulti, {
88
TypeaheadInputMultiProps,
99
} from './TypeaheadInputMulti';
1010

11-
import options from '../../tests/data';
11+
import options, {TestOption} from '../../tests/data';
1212
import { HintProvider, noop } from '../../tests/helpers';
1313
import type { Size } from '../../types';
1414
import {OptionType} from "../../types";
@@ -36,7 +36,7 @@ interface Args<Option extends OptionType> extends TypeaheadInputMultiProps<Optio
3636
size?: Size;
3737
}
3838

39-
const Template: Story<Args> = ({ hintText = '', ...args }) => {
39+
const Template: Story<Args<TestOption>> = ({ hintText = '', ...args }) => {
4040
const [value, setValue] = useState(args.value);
4141
const [inputNode, setInputNode] = useState<HTMLInputElement | null>(null);
4242

src/components/TypeaheadMenu/TypeaheadMenu.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const propTypes = {
4242
const defaultProps = {
4343
newSelectionPrefix: 'New selection: ',
4444
paginationText: 'Display additional results...',
45-
renderMenuItemChildren: (option: OptionType, props: TypeaheadMenuProps<OptionType>) => (
45+
renderMenuItemChildren: <Option extends OptionType>(option: Option, props: TypeaheadMenuProps<Option>) => (
4646
<Highlighter search={props.text}>
4747
{getOptionLabel(option, props.labelKey)}
4848
</Highlighter>

src/core/Context.tsx

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createContext, useContext } from 'react';
22

3-
import { noop } from '../utils';
3+
import {noop, once} from '../utils';
44
import { Id, OptionType, OptionHandler, SelectEvent } from '../types';
55

66
export interface TypeaheadContextType<Option extends OptionType> {
@@ -31,7 +31,5 @@ export const defaultContext = {
3131
setItem: noop,
3232
};
3333

34-
export const TypeaheadContext =
35-
createContext<TypeaheadContextType<OptionType>>(defaultContext);
36-
37-
export const useTypeaheadContext = () => useContext(TypeaheadContext);
34+
export const createTypeaheadContext = once(<Option extends OptionType>() => createContext<TypeaheadContextType<Option>>(defaultContext));
35+
export const useTypeaheadContext = <Option extends OptionType>() => useContext(createTypeaheadContext<Option>());

src/core/TypeaheadManager.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { KeyboardEvent, useEffect, useRef } from 'react';
22

3-
import { TypeaheadContext, TypeaheadContextType } from './Context';
3+
import {createTypeaheadContext, TypeaheadContextType} from './Context';
44
import {
55
defaultSelectHint,
66
getHintText,
@@ -55,6 +55,8 @@ const contextKeys = [
5555
] as (keyof TypeaheadManagerProps<OptionType>)[];
5656

5757
const TypeaheadManager = <Option extends OptionType>(props: TypeaheadManagerProps<Option>) => {
58+
const TypeaheadContext = createTypeaheadContext<Option>()
59+
5860
const {
5961
allowNew,
6062
children,

src/core/TypeaheadState.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { TypeaheadProps, TypeaheadState } from '../types';
1+
import {OptionType, TypeaheadProps, TypeaheadState} from '../types';
22
import { getOptionLabel } from '../utils';
33

4-
type Props = TypeaheadProps;
4+
type Props<Option extends OptionType> = TypeaheadProps<Option>;
55

6-
export function getInitialState(props: Props): TypeaheadState {
6+
export function getInitialState<Option extends OptionType>(props: Props<Option>): TypeaheadState<Option> {
77
const {
88
defaultInputValue,
99
defaultOpen,
@@ -40,7 +40,7 @@ export function getInitialState(props: Props): TypeaheadState {
4040
};
4141
}
4242

43-
export function clearTypeahead(state: TypeaheadState, props: Props) {
43+
export function clearTypeahead<Option extends OptionType>(state: TypeaheadState<Option>, props: Props<Option>) {
4444
return {
4545
...getInitialState(props),
4646
isFocused: state.isFocused,
@@ -49,15 +49,15 @@ export function clearTypeahead(state: TypeaheadState, props: Props) {
4949
};
5050
}
5151

52-
export function clickOrFocusInput(state: TypeaheadState) {
52+
export function clickOrFocusInput<Option extends OptionType>(state: TypeaheadState<Option>) {
5353
return {
5454
...state,
5555
isFocused: true,
5656
showMenu: true,
5757
};
5858
}
5959

60-
export function hideMenu(state: TypeaheadState, props: Props) {
60+
export function hideMenu<Option extends OptionType>(state: TypeaheadState<Option>, props: Props<Option>) {
6161
const { activeIndex, activeItem, initialItem, shownResults } =
6262
getInitialState(props);
6363

@@ -71,6 +71,6 @@ export function hideMenu(state: TypeaheadState, props: Props) {
7171
};
7272
}
7373

74-
export function toggleMenu(state: TypeaheadState, props: Props) {
74+
export function toggleMenu<Option extends OptionType>(state: TypeaheadState<Option>, props: Props<Option>) {
7575
return state.showMenu ? hideMenu(state, props) : { ...state, showMenu: true };
7676
}

src/tests/helpers.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ import { composeStories, composeStory } from '@storybook/testing-react';
99
import { screen } from '@testing-library/react';
1010

1111
import {
12+
createTypeaheadContext,
1213
defaultContext,
13-
TypeaheadContext,
1414
TypeaheadContextType,
1515
} from '../core/Context';
16+
import {OptionType} from "../types";
1617

1718
export { axe };
1819
export * from '@storybook/testing-react';
@@ -44,11 +45,12 @@ export function generateSnapshots(stories: StoriesImport) {
4445
});
4546
}
4647

47-
interface HintProviderProps extends Partial<TypeaheadContextType> {
48+
interface HintProviderProps<Option extends OptionType> extends Partial<TypeaheadContextType<Option>> {
4849
children?: ReactNode;
4950
}
5051

51-
export const HintProvider = ({ children, ...context }: HintProviderProps) => {
52+
export const HintProvider = <Option extends OptionType>({ children, ...context }: HintProviderProps<Option>) => {
53+
const TypeaheadContext = createTypeaheadContext<Option>()
5254
return (
5355
<TypeaheadContext.Provider
5456
value={{

src/utils/defaultFilterBy.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,17 @@ import warn from './warn';
77

88
import type { LabelKey, OptionType } from '../types';
99

10-
interface Props {
10+
interface Props<Option extends OptionType> {
1111
caseSensitive: boolean;
1212
filterBy: string[];
1313
ignoreDiacritics: boolean;
14-
labelKey: LabelKey;
14+
labelKey: LabelKey<Option>;
1515
multiple: boolean;
16-
selected: OptionType[];
16+
selected: Option[];
1717
text: string;
1818
}
1919

20-
function isMatch(input: string, string: string, props: Props): boolean {
20+
function isMatch<Option extends OptionType>(input: string, string: string, props: Props<Option>): boolean {
2121
let searchStr = input;
2222
let str = string;
2323

@@ -37,7 +37,7 @@ function isMatch(input: string, string: string, props: Props): boolean {
3737
/**
3838
* Default algorithm for filtering results.
3939
*/
40-
export default function defaultFilterBy(option: OptionType, props: Props): boolean {
40+
export default function defaultFilterBy<Option extends OptionType>(option: Option, props: Props<Option>): boolean {
4141
const { filterBy, labelKey, multiple, selected, text } = props;
4242

4343
// Don't show selected options in the menu for the multi-select case.

src/utils/getHintText.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@ import { isString } from './nodash';
55

66
import { LabelKey, OptionType } from '../types';
77

8-
interface HintProps {
8+
interface HintProps<Option extends OptionType> {
99
activeIndex: number;
10-
initialItem?: OptionType;
10+
initialItem?: Option;
1111
isFocused: boolean;
1212
isMenuShown: boolean;
13-
labelKey: LabelKey;
13+
labelKey: LabelKey<Option>;
1414
multiple: boolean;
15-
selected: OptionType[];
15+
selected: Option[];
1616
text: string;
1717
}
1818

19-
function getHintText({
19+
function getHintText<Option extends OptionType>({
2020
activeIndex,
2121
initialItem,
2222
isFocused,
@@ -25,7 +25,7 @@ function getHintText({
2525
multiple,
2626
selected,
2727
text,
28-
}: HintProps) {
28+
}: HintProps<Option>) {
2929
// Don't display a hint under the following conditions:
3030
if (
3131
// No text entered.

src/utils/getInputText.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import getOptionLabel from './getOptionLabel';
22
import { LabelKey, OptionType } from '../types';
33

4-
interface Props {
5-
activeItem?: OptionType;
6-
labelKey: LabelKey;
4+
interface Props<Option extends OptionType> {
5+
activeItem?: Option;
6+
labelKey: LabelKey<Option>;
77
multiple: boolean;
8-
selected: OptionType[];
8+
selected: Option[];
99
text: string;
1010
}
1111

12-
function getInputText(props: Props): string {
12+
function getInputText<Option extends OptionType>(props: Props<Option>): string {
1313
const { activeItem, labelKey, multiple, selected, text } = props;
1414

1515
if (activeItem) {

src/utils/getIsOnlyResult.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import getOptionProperty from './getOptionProperty';
22

33
import { AllowNew, OptionType } from '../types';
44

5-
interface Props {
6-
allowNew: AllowNew;
5+
interface Props<Option extends OptionType> {
6+
allowNew: AllowNew<Option>;
77
highlightOnlyResult: boolean;
8-
results: OptionType[];
8+
results: Option[];
99
}
1010

11-
function getIsOnlyResult(props: Props): boolean {
11+
function getIsOnlyResult<Option extends OptionType>(props: Props<Option>): boolean {
1212
const { allowNew, highlightOnlyResult, results } = props;
1313

1414
if (!highlightOnlyResult || allowNew) {

0 commit comments

Comments
 (0)