Skip to content

Commit

Permalink
fix(ComboBox): drop trigger ref
Browse files Browse the repository at this point in the history
  • Loading branch information
LamaEats committed Jul 14, 2023
1 parent dbf27e2 commit b53d4b6
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 147 deletions.
283 changes: 138 additions & 145 deletions src/components/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@ import { flatten } from '../utils/flatten';

import { Popup } from './Popup';

interface ComboBoxTriggerProps<T extends HTMLElement = HTMLButtonElement, R extends React.Ref<T> = React.Ref<T>> {
text: ComboBoxProps<T>['text'];
value: ComboBoxProps<T>['value'];
ref: R;
interface ComboBoxTriggerProps {
text: ComboBoxProps['text'];
value: ComboBoxProps['value'];
disabled?: boolean;

onClick: () => void;
Expand All @@ -34,10 +33,10 @@ interface ComboBoxItemProps {
onClick: (value?: any) => void;
}

interface ComboBoxProps<T extends HTMLElement> {
interface ComboBoxProps {
renderInput: (props: ComboBoxInputProps) => React.ReactNode;
renderItem: (props: ComboBoxItemProps) => React.ReactNode | Record<any, any>;
renderTrigger?: (props: ComboBoxTriggerProps<T>) => React.ReactNode;
renderTrigger?: (props: ComboBoxTriggerProps) => React.ReactNode;
renderItems?: (children: React.ReactNode | Array<Record<any, any>> | undefined) => React.ReactNode;
text?: string;
value?: any;
Expand Down Expand Up @@ -73,150 +72,144 @@ const StyledErrorTrigger = styled.div`
z-index: 1;
`;

export function ComboBoxRenderFunction<T extends HTMLElement>(
props: ComboBoxProps<T>,
ref: React.ForwardedRef<HTMLDivElement>,
): React.ReactElement {
const {
text,
value,
visible = false,
items = [],
disabled,
error,
maxWidth = 250,
minWidth = 150,
className,
placement = 'bottom-start',
offset = [-4, 8],
renderItem,
renderTrigger,
renderInput,
renderItems,
onChange,
onClose,
onClickOutside,
} = props;
const popupContentRef = useRef<HTMLDivElement>(null);
const popupRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<T>(null);
const inputRef = useRef<HTMLInputElement>(null);
const [popupVisible, setPopupVisibility] = useState(visible);
const [editMode, setEditMode] = useState(false);
const downPress = useKeyPress('ArrowDown');
const upPress = useKeyPress('ArrowUp');
const [cursor, setCursor] = useState(0);
const flatItems = useMemo(() => flatten(items), [items]);

useEffect(() => {
setPopupVisibility(visible);
}, [visible]);

useEffect(() => {
if (renderTrigger) {
setPopupVisibility(editMode);
}
}, [renderTrigger, editMode]);

const onTriggerClick = useCallback(() => {
setEditMode(true);
}, []);

const onItemClick = useCallback(
(value: any) => () => {
setEditMode(false);
onChange?.(value);
export const ComboBox = forwardRef<HTMLDivElement, ComboBoxProps>(
(
{
text,
value,
visible = false,
items = [],
disabled,
error,
maxWidth = 250,
minWidth = 150,
className,
placement = 'bottom-start',
offset = [-4, 8],
renderItem,
renderTrigger,
renderInput,
renderItems,
onChange,
onClose,
onClickOutside,
},
[onChange],
);

const [onESC] = useKeyboard([KeyCode.Escape], () => {
setEditMode(false);
onClose?.();
});

const [onENTER] = useKeyboard([KeyCode.Enter], () => {
onItemClick(flatItems[cursor])();
});

useEffect(() => {
if (flatItems.length && downPress) {
setCursor((prevState) => (prevState < flatItems.length - 1 ? prevState + 1 : prevState));
}
}, [flatItems, downPress]);

useEffect(() => {
if (flatItems.length && upPress) {
setCursor((prevState) => (prevState > 0 ? prevState - 1 : prevState));
}
}, [flatItems, upPress]);

const onErrorMouseEnter = useCallback(() => setPopupVisibility(true), []);
const onErrorMouseLeave = useCallback(() => setPopupVisibility(false), []);

useClickOutside(inputRef, (e) => {
onClickOutside?.(() => {
// popup is outside of component
if (!popupContentRef.current?.contains(e.target as Node)) {
ref,
) => {
const popupContentRef = useRef<HTMLDivElement>(null);
const popupRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const [popupVisible, setPopupVisibility] = useState(visible);
const [editMode, setEditMode] = useState(false);
const downPress = useKeyPress('ArrowDown');
const upPress = useKeyPress('ArrowUp');
const [cursor, setCursor] = useState(0);
const flatItems = useMemo(() => flatten(items), [items]);

useEffect(() => {
setPopupVisibility(visible);
}, [visible]);

useEffect(() => {
if (renderTrigger) {
setPopupVisibility(editMode);
}
}, [renderTrigger, editMode]);

const onTriggerClick = useCallback(() => {
setEditMode(true);
}, []);

const onItemClick = useCallback(
(value: any) => () => {
setEditMode(false);
onClose?.();
onChange?.(value);
},
[onChange],
);

const [onESC] = useKeyboard([KeyCode.Escape], () => {
setEditMode(false);
onClose?.();
});

const [onENTER] = useKeyboard([KeyCode.Enter], () => {
onItemClick(flatItems[cursor])();
});

useEffect(() => {
if (flatItems.length && downPress) {
setCursor((prevState) => (prevState < flatItems.length - 1 ? prevState + 1 : prevState));
}
}, [flatItems, downPress]);

useEffect(() => {
if (flatItems.length && upPress) {
setCursor((prevState) => (prevState > 0 ? prevState - 1 : prevState));
}
}, [flatItems, upPress]);

const onErrorMouseEnter = useCallback(() => setPopupVisibility(true), []);
const onErrorMouseLeave = useCallback(() => setPopupVisibility(false), []);

useClickOutside(inputRef, (e) => {
onClickOutside?.(() => {
// popup is outside of component
if (!popupContentRef.current?.contains(e.target as Node)) {
setEditMode(false);
onClose?.();
}
});
});
});

const children = flatItems.map((item: any, index: number) =>
renderItem({ item, index, cursor, onClick: onItemClick(item) }),
);

return (
<StyledComboBox ref={ref} className={className}>
{nullable(error, (err) => (
<>
<StyledErrorTrigger
ref={popupRef}
onMouseEnter={onErrorMouseEnter}
onMouseLeave={onErrorMouseLeave}
/>
<Popup tooltip view="danger" placement="top-start" visible={popupVisible} reference={popupRef}>
{err.message}
</Popup>
</>
))}

<span ref={popupRef} {...onESC}>
{renderTrigger ? (
<>
{editMode
? renderInput({ value, disabled, ref: inputRef, ...onENTER })
: renderTrigger({ text, value, disabled, ref: triggerRef, onClick: onTriggerClick })}
</>
) : (
renderInput({ value, disabled, ref: inputRef, ...onENTER })
)}
</span>

<Popup
placement={placement}
visible={popupVisible && Boolean(flatItems.length)}
reference={popupRef}
interactive
arrow={false}
minWidth={minWidth}
maxWidth={maxWidth}
offset={offset}
>
<div ref={popupContentRef} {...onESC}>
{renderItems ? renderItems(children as React.ReactNode) : (children as React.ReactNode)}
</div>
</Popup>
</StyledComboBox>
);
}

type CustomForwardRefResult = <T extends HTMLElement>(
props: React.PropsWithoutRef<ComboBoxProps<T>> & React.RefAttributes<HTMLDivElement>,
) => React.ReactElement;
const children = flatItems.map((item: any, index: number) =>
renderItem({ item, index, cursor, onClick: onItemClick(item) }),
);

export const ComboBox = forwardRef(ComboBoxRenderFunction) as CustomForwardRefResult;
return (
<StyledComboBox ref={ref} className={className}>
{nullable(error, (err) => (
<>
<StyledErrorTrigger
ref={popupRef}
onMouseEnter={onErrorMouseEnter}
onMouseLeave={onErrorMouseLeave}
/>
<Popup tooltip view="danger" placement="top-start" visible={popupVisible} reference={popupRef}>
{err.message}
</Popup>
</>
))}

<span ref={popupRef} {...onESC}>
{renderTrigger ? (
<>
{editMode
? renderInput({ value, disabled, ref: inputRef, ...onENTER })
: renderTrigger({ text, value, disabled, onClick: onTriggerClick })}
</>
) : (
renderInput({ value, disabled, ref: inputRef, ...onENTER })
)}
</span>

<Popup
placement={placement}
visible={popupVisible && Boolean(flatItems.length)}
reference={popupRef}
interactive
arrow={false}
minWidth={minWidth}
maxWidth={maxWidth}
offset={offset}
>
<div ref={popupContentRef} {...onESC}>
{renderItems ? renderItems(children as React.ReactNode) : (children as React.ReactNode)}
</div>
</Popup>
</StyledComboBox>
);
},
);

export default ComboBox;
4 changes: 2 additions & 2 deletions src/components/FormMultiInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const StyledInput = styled(Input)`
min-width: 100px;
`;

const StyledComboBox = styled(ComboBox<HTMLButtonElement>)`
const StyledComboBox = styled(ComboBox)`
margin-left: ${gapS};
`;

Expand Down Expand Up @@ -120,7 +120,7 @@ export const FormMultiInput = React.forwardRef<HTMLDivElement, FormMultiInputPro
disabled={disabled}
onChange={onValueAdd}
items={items}
renderTrigger={(props) => <PlusIcon ref={props.ref} size="xs" onClick={props.onClick} />}
renderTrigger={(props) => <PlusIcon size="xs" onClick={props.onClick} />}
renderInput={(props) => (
<StyledInput
autoFocus
Expand Down

0 comments on commit b53d4b6

Please sign in to comment.