Skip to content

Commit

Permalink
Add generic option type
Browse files Browse the repository at this point in the history
  • Loading branch information
HansAarneLiblik committed Feb 27, 2023
1 parent 56050e3 commit 53a0c70
Show file tree
Hide file tree
Showing 29 changed files with 167 additions and 146 deletions.
8 changes: 4 additions & 4 deletions src/behaviors/async.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { optionType } from '../propTypes';
import { getDisplayName, isFunction } from '../utils';

import { TypeaheadComponentProps } from '../components/Typeahead';
import type { Option } from '../types';
import type { OptionType } from '../types';

const propTypes = {
/**
Expand Down Expand Up @@ -57,7 +57,7 @@ const propTypes = {
useCache: PropTypes.bool,
};

export interface UseAsyncProps extends TypeaheadComponentProps {
export interface UseAsyncProps<Option extends OptionType> extends TypeaheadComponentProps<Option> {
delay?: number;
isLoading: boolean;
onSearch: (query: string) => void;
Expand All @@ -66,7 +66,7 @@ export interface UseAsyncProps extends TypeaheadComponentProps {
useCache?: boolean;
}

type Cache = Record<string, Option[]>;
type Cache = Record<string, OptionType[]>;

interface DebouncedFunction extends Function {
cancel(): void;
Expand All @@ -80,7 +80,7 @@ interface DebouncedFunction extends Function {
* - Optional query caching
* - Search prompt and empty results behaviors
*/
export function useAsync(props: UseAsyncProps) {
export function useAsync<Option extends OptionType>(props: UseAsyncProps<Option>) {
const {
allowNew,
delay = 200,
Expand Down
4 changes: 2 additions & 2 deletions src/behaviors/item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { useTypeaheadContext } from '../core/Context';
import { getDisplayName, getMenuItemId, preventInputBlur } from '../utils';

import { optionType } from '../propTypes';
import { Option } from '../types';
import { OptionType } from '../types';

const propTypes = {
option: optionType.isRequired,
Expand All @@ -23,7 +23,7 @@ const propTypes = {

export interface UseItemProps<T> extends HTMLProps<T> {
onClick?: MouseEventHandler<T>;
option: Option;
option: OptionType;
position: number;
}

Expand Down
10 changes: 5 additions & 5 deletions src/behaviors/token.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ import { useRootClose } from 'react-overlays';
import { getDisplayName, isFunction } from '../utils';

import { optionType } from '../propTypes';
import { Option, OptionHandler, RefElement } from '../types';
import { OptionType, OptionHandler, RefElement } from '../types';

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

Expand All @@ -34,14 +34,14 @@ const propTypes = {
option: optionType.isRequired,
};

export function useToken<T extends HTMLElement>({
export function useToken<T extends HTMLElement, Option extends OptionType>({
onBlur,
onClick,
onFocus,
onRemove,
option,
...props
}: UseTokenProps<T>) {
}: UseTokenProps<T, Option>) {
const [active, setActive] = useState<boolean>(false);
const [rootElement, attachRef] = useState<RefElement<T>>(null);

Expand Down
3 changes: 2 additions & 1 deletion src/components/MenuItem/MenuItem.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import {
TypeaheadContext,
TypeaheadContextType,
} from '../../core/Context';
import {OptionType} from "../../types";

export default {
title: 'Components/MenuItem/MenuItem',
component: MenuItem,
} as Meta;

interface Args {
context: Partial<TypeaheadContextType>;
context: Partial<TypeaheadContextType<OptionType>>;
props: MenuItemProps;
}

Expand Down
3 changes: 2 additions & 1 deletion src/components/Token/Token.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import { Story, Meta } from '@storybook/react';

import Token, { TokenProps } from './Token';
import { noop } from '../../utils';
import {OptionType} from "../../types";

export default {
title: 'Components/Token',
component: Token,
} as Meta;

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

export const Interactive = Template.bind({});
Interactive.args = {
Expand Down
7 changes: 4 additions & 3 deletions src/components/Token/Token.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ClearButton from '../ClearButton';

import { useToken, UseTokenProps } from '../../behaviors/token';
import { isFunction } from '../../utils';
import {OptionType} from "../../types";

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

Expand Down Expand Up @@ -68,7 +69,7 @@ const StaticToken = ({
return <div className={classnames}>{children}</div>;
};

export interface TokenProps<T> extends UseTokenProps<T> {
export interface TokenProps<T, Option extends OptionType> extends UseTokenProps<T, Option> {
disabled?: boolean;
readOnly?: boolean;
}
Expand All @@ -77,12 +78,12 @@ export interface TokenProps<T> extends UseTokenProps<T> {
* Individual token component, generally displayed within the
* `TypeaheadInputMulti` component, but can also be rendered on its own.
*/
const Token = ({
const Token = <Option extends OptionType>({
children,
option,
readOnly,
...props
}: TokenProps<HTMLElement>) => {
}: TokenProps<HTMLElement, Option>) => {
const { ref, ...tokenProps } = useToken({ ...props, option });
const child = <div className="rbt-token-label">{children}</div>;

Expand Down
11 changes: 6 additions & 5 deletions src/components/Typeahead/Typeahead.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import Hint from '../Hint';
import Menu from '../Menu';
import MenuItem from '../MenuItem';

import options, { Option } from '../../tests/data';
import options, { TestOption } from '../../tests/data';
import { noop } from '../../tests/helpers';
import {OptionType} from "../../types";

export default {
title: 'Components/Typeahead',
Expand Down Expand Up @@ -53,7 +54,7 @@ const defaultProps = {
positionFixed: true,
};

const Template: Story<TypeaheadComponentProps> = (args) => (
const Template: Story<TypeaheadComponentProps<TestOption>> = (args) => (
<Typeahead {...args} />
);

Expand Down Expand Up @@ -122,7 +123,7 @@ CustomMenu.args = {
renderMenu: (results, menuProps) => (
<Menu {...menuProps}>
{/* Use `slice` to avoid mutating the original array */}
{(results as Option[])
{(results as TestOption[])
.slice()
.reverse()
.map((r, index) => (
Expand All @@ -134,7 +135,7 @@ CustomMenu.args = {
),
};

export const InputGrouping = (args: TypeaheadComponentProps) => (
export const InputGrouping = <Option extends OptionType>(args: TypeaheadComponentProps<Option>) => (
<div
className={cx('input-group', {
'input-group-sm': args.size === 'sm',
Expand All @@ -149,7 +150,7 @@ InputGrouping.args = {
...defaultProps,
};

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

return <Typeahead {...args} onChange={setSelected} selected={selected} />;
Expand Down
4 changes: 2 additions & 2 deletions src/components/Typeahead/Typeahead.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ import {
waitFor,
} from '../../tests/helpers';

import states from '../../tests/data';
import states, {TestOption} from '../../tests/data';

const ID = 'rbt-id';

const TestComponent = forwardRef<Typeahead, Partial<TypeaheadComponentProps>>(
const TestComponent = forwardRef<Typeahead<TestOption>, Partial<TypeaheadComponentProps<TestOption>>>(
(props, ref) => (
<TypeaheadComponent
id={ID}
Expand Down
64 changes: 38 additions & 26 deletions src/components/Typeahead/Typeahead.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
import { checkPropType, inputPropsType, sizeType } from '../../propTypes';

import {
Option,
OptionType,
RefElement,
RenderToken,
RenderTokenProps,
Expand All @@ -42,23 +42,23 @@ import {
TypeaheadManagerChildProps,
} from '../../types';

export interface RenderMenuProps
export interface RenderMenuProps<Option extends OptionType>
extends Omit<
TypeaheadMenuProps,
TypeaheadMenuProps<Option>,
'labelKey' | 'options' | 'renderMenuItemChildren' | 'text'
> {
renderMenuItemChildren?: RenderMenuItemChildren;
renderMenuItemChildren?: RenderMenuItemChildren<Option>;
}

export interface TypeaheadComponentProps extends Partial<TypeaheadProps> {
export interface TypeaheadComponentProps<Option extends OptionType> extends Partial<TypeaheadProps<Option>> {
align?: Align;
className?: string;
clearButton?: boolean;
disabled?: boolean;
dropup?: boolean;
emptyLabel?: ReactNode;
flip?: boolean;
instanceRef?: Ref<Typeahead>;
instanceRef?: Ref<Typeahead<Option>>;
isInvalid?: boolean;
isLoading?: boolean;
isValid?: boolean;
Expand All @@ -70,15 +70,15 @@ export interface TypeaheadComponentProps extends Partial<TypeaheadProps> {
positionFixed?: boolean;
renderInput?: (
inputProps: TypeaheadInputProps,
props: TypeaheadManagerChildProps
props: TypeaheadManagerChildProps<Option>
) => JSX.Element;
renderMenu?: (
results: Option[],
menuProps: RenderMenuProps,
state: TypeaheadManagerChildProps
menuProps: RenderMenuProps<Option>,
state: TypeaheadManagerChildProps<Option>
) => JSX.Element;
renderMenuItemChildren?: RenderMenuItemChildren;
renderToken?: RenderToken;
renderMenuItemChildren?: RenderMenuItemChildren<Option>;
renderToken?: RenderToken<Option>;
size?: Size;
style?: CSSProperties;
}
Expand Down Expand Up @@ -127,10 +127,10 @@ const defaultProps = {
isLoading: false,
};

const defaultRenderMenu = (
const defaultRenderMenu = <Option extends OptionType>(
results: Option[],
menuProps: RenderMenuProps,
props: TypeaheadManagerChildProps
menuProps: RenderMenuProps<Option>,
props: TypeaheadManagerChildProps<Option>
) => (
<TypeaheadMenu
{...menuProps}
Expand All @@ -140,9 +140,9 @@ const defaultRenderMenu = (
/>
);

const defaultRenderToken = (
const defaultRenderToken = <Option extends OptionType>(
option: Option,
props: RenderTokenProps,
props: RenderTokenProps<Option>,
idx: number
) => (
<Token
Expand All @@ -160,9 +160,9 @@ const overlayPropKeys = [
'dropup',
'flip',
'positionFixed',
] as (keyof TypeaheadComponentProps)[];
] as (keyof TypeaheadComponentProps<OptionType>)[];

function getOverlayProps(props: TypeaheadComponentProps) {
function getOverlayProps<Option extends OptionType>(props: TypeaheadComponentProps<Option>) {
return pick(props, overlayPropKeys);
}

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

class TypeaheadComponent extends React.Component<TypeaheadComponentProps> {
class TypeaheadComponent<Option extends OptionType> extends React.Component<TypeaheadComponentProps<Option>> {
static propTypes = propTypes;
static defaultProps = defaultProps;

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

return (
<Typeahead {...this.props} options={options} ref={instanceRef}>
{(props: TypeaheadManagerChildProps) => {
{(props: TypeaheadManagerChildProps<Option>) => {
const { hideMenu, isMenuShown, results } = props;
const auxContent = this._renderAux(props);

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

_renderInput = (
inputProps: TypeaheadInputProps,
props: TypeaheadManagerChildProps
props: TypeaheadManagerChildProps<Option>
) => {
const { isInvalid, isValid, multiple, renderInput, renderToken, size } =
this.props;
Expand Down Expand Up @@ -279,7 +279,7 @@ class TypeaheadComponent extends React.Component<TypeaheadComponentProps> {
_renderMenu = (
results: Option[],
menuProps: OverlayRenderProps,
props: TypeaheadManagerChildProps
props: TypeaheadManagerChildProps<Option>
) => {
const {
emptyLabel,
Expand All @@ -306,7 +306,7 @@ class TypeaheadComponent extends React.Component<TypeaheadComponentProps> {
);
};

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

let content;
Expand All @@ -331,6 +331,18 @@ class TypeaheadComponent extends React.Component<TypeaheadComponentProps> {
};
}

export default forwardRef<Typeahead, TypeaheadComponentProps>((props, ref) => (
<TypeaheadComponent {...props} instanceRef={ref} />
));
const TypeaheadComponentInner = <Option extends OptionType>(props: TypeaheadComponentProps<Option>, ref: React.ForwardedRef<Typeahead<Option>>) => <TypeaheadComponent {...props} instanceRef={ref} />

const TypeaheadComponentWithRef = forwardRef(TypeaheadComponentInner);

type TypeaheadComponentWithRefProps<Option extends OptionType> = TypeaheadComponentProps<Option> & {
ref?: React.Ref<Typeahead<Option>>;
};

export default function TypeaheadComp<Option extends OptionType>({
ref,
...props
}: TypeaheadComponentWithRefProps<Option>) {
// @ts-ignore
return <TypeaheadComponentWithRef ref={ref} {...props} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import TypeaheadInputMulti, {
import options from '../../tests/data';
import { HintProvider, noop } from '../../tests/helpers';
import type { Size } from '../../types';
import {OptionType} from "../../types";

export default {
title: 'Components/TypeaheadInputMulti',
Expand All @@ -28,7 +29,7 @@ const defaultProps = {
selected,
};

interface Args extends TypeaheadInputMultiProps {
interface Args<Option extends OptionType> extends TypeaheadInputMultiProps<Option> {
hintText?: string;
isValid?: boolean;
isInvalid?: boolean;
Expand Down
Loading

0 comments on commit 53a0c70

Please sign in to comment.