Skip to content

Commit

Permalink
fix: autocomplete shuold be possible to use inside 'small' scrollable…
Browse files Browse the repository at this point in the history
… components (argoproj#122)

Signed-off-by: Alexander Matyushentsev <[email protected]>
  • Loading branch information
Alexander Matyushentsev authored Aug 10, 2021
1 parent be4673f commit 9ebea33
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 472 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@
"jest": "^26.6.2",
"jscs": "^3.0.7",
"node-sass": "^4.12.0",
"nodemon": "^1.14.11",
"raw-loader": "^0.5.1",
"react": "^16.9.3",
"react-dom": "^16.9.3",
Expand Down
6 changes: 1 addition & 5 deletions v2/components/autocomplete/autocomplete.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

&__items {
z-index: 3;
position: absolute;
position: fixed;
white-space: nowrap;
max-height: 12em;
overflow-y: auto;
Expand All @@ -20,10 +20,6 @@
box-shadow: 1px 2px 2px rgba(0, 0, 0, 0.05);
line-height: 0.5em;

&--inverted {
bottom: 50px;
}

&__item {
z-index: 2;
padding: 0.75em 0;
Expand Down
2 changes: 1 addition & 1 deletion v2/components/autocomplete/autocomplete.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default {

export const Primary = (args: any) => (
<div style={{width: '50%', paddingBottom: '6em'}}>
<Autocomplete items={['hello', 'world']} {...args} />
<Autocomplete {...args} items={['hello', 'world']} />
</div>
);

Expand Down
114 changes: 63 additions & 51 deletions v2/components/autocomplete/autocomplete.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Key, KeybindingContext, KeybindingProvider, useNav} from '../../shared';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import {Key, KeybindingContext, KeybindingProvider, useNav} from '../../shared';
import {Input, InputProps, SetInputFxn, useDebounce, useInput} from '../input/input';
import ThemeDiv from '../theme-div/theme-div';

Expand All @@ -9,14 +10,14 @@ interface AutocompleteProps extends InputProps {
inputref?: React.MutableRefObject<HTMLInputElement>;
}

export const useAutocomplete = (init: string, callback?: (val: string) => void): [string, SetInputFxn, AutocompleteProps] => {
const [state, setState, Input] = useInput(init);
const Autocomplete = Input as AutocompleteProps;
if (Autocomplete.ref) {
Autocomplete.inputref = Input.ref;
delete Autocomplete.ref;
export const useAutocomplete = (init: string): [string, SetInputFxn, AutocompleteProps] => {
const [state, setState, input] = useInput(init);
const autocomplete = input as AutocompleteProps;
if (autocomplete.ref) {
autocomplete.inputref = input.ref;
delete autocomplete.ref;
}
return [state, setState, Autocomplete];
return [state, setState, autocomplete];
};

export const Autocomplete = (
Expand All @@ -26,7 +27,7 @@ export const Autocomplete = (
onItemClick?: (item: string) => void;
icon?: string;
inputref?: React.MutableRefObject<HTMLInputElement>;
}
},
) => {
return (
<KeybindingProvider>
Expand All @@ -42,19 +43,20 @@ export const RenderAutocomplete = (
onItemClick?: (item: string) => void;
icon?: string;
inputref?: React.MutableRefObject<HTMLInputElement>;
}
},
) => {
const [curItems, setCurItems] = React.useState(props.items || []);
const nullInputRef = React.useRef<HTMLInputElement>(null);
const inputRef = props.inputref || nullInputRef;
const autocompleteRef = React.useRef(null);
const [showSuggestions, setShowSuggestions] = React.useState(false);
const [pos, nav, reset] = useNav(props.items.length);
const [pos, nav, reset] = useNav(props.items?.length);
const menuRef = React.useRef(null);

React.useEffect(() => {
function unfocus(e: any) {
if (autocompleteRef.current && !autocompleteRef.current.contains(e.target)) {
if (autocompleteRef.current && !autocompleteRef.current.contains(e.target) &&
menuRef.current && !menuRef.current.contains(e.target)) {
setShowSuggestions(false);
reset();
}
Expand All @@ -69,7 +71,7 @@ export const RenderAutocomplete = (
React.useEffect(() => {
const filtered = (props.items || []).filter((i) => {
if (i) {
return i.toLowerCase().includes(debouncedVal.toLowerCase());
return i.toLowerCase().includes(debouncedVal?.toLowerCase());
}
return false;
});
Expand Down Expand Up @@ -137,34 +139,42 @@ export const RenderAutocomplete = (
delete trimmedProps.inputStyle;
delete trimmedProps.onItemClick;

const [inverted, setInverted] = React.useState(false);
const [position, setPosition] = React.useState({top: 0, left: 0});

const checkDirection = (e: any) => {
const checkDirection = () => {
if (autocompleteRef && autocompleteRef.current && menuRef.current) {
if (inputRef.current && menuRef.current) {
const rect = inputRef.current.getBoundingClientRect();
const menuHeight = menuRef.current.clientHeight;
const offset = window.innerHeight - inputRef.current.getBoundingClientRect().bottom;
if (offset < menuHeight) {
if (!inverted) {
setInverted(true);
}
} else {
if (inverted) {
setInverted(false);
}
const offset = window.innerHeight - rect.bottom;
const inverted = offset < menuHeight;

const newPos = {
top: inverted ? rect.top - menuRef.current.clientHeight : rect.top + rect.height,
left: rect.left,
};
if (position.left !== newPos.left || position.top !== newPos.top) {
setPosition(newPos);
}
}
}
};

React.useEffect(() => {
checkDirection();
document.addEventListener('scroll', checkDirection, true);
document.addEventListener('resize', checkDirection, true);
return () => {
document.removeEventListener('scroll', checkDirection);
document.removeEventListener('resize', checkDirection);
};
});
}, []);

const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (props.onChange) {
props.onChange(e);
}
};

return (
<div className='autocomplete' ref={autocompleteRef} style={style as any}>
Expand All @@ -173,36 +183,38 @@ export const RenderAutocomplete = (
style={props.inputStyle}
innerref={inputRef}
className={(props.className || '') + ' autocomplete__input'}
onChange={(e) => {
if (props.onChange) {
props.onChange(e);
}
}}
onFocus={(e) => {
onChange={onChange}
onFocus={() => {
setShowSuggestions(true);
checkDirection(e);
checkDirection();
}}
/>

<ThemeDiv
className={`autocomplete__items ${inverted ? 'autocomplete__items--inverted' : ''}`}
style={!showSuggestions || (props.items || []).length < 1 ? {visibility: 'hidden'} : {visibility: 'visible'}}
innerref={menuRef}>
{(curItems || []).map((i, n) => (
<div
key={i}
onClick={() => {
props.onChange({target: {value: i}} as React.ChangeEvent<HTMLInputElement>);
setShowSuggestions(false);
if (props.onItemClick) {
props.onItemClick(i);
}
}}
className={`autocomplete__items__item ${pos === n ? 'autocomplete__items__item--selected' : ''}`}>
{i}
</div>
))}
</ThemeDiv>
{ReactDOM.createPortal((
<ThemeDiv
className='autocomplete__items'
style={{
visibility: !showSuggestions || (props.items || []).length < 1 ? 'hidden' : 'visible',
top: position.top,
left: position.left,
}}
innerref={menuRef}>
{(curItems || []).map((i, n) => (
<div
key={i}
onClick={() => {
onChange({target: {value: i}} as React.ChangeEvent<HTMLInputElement>);
setShowSuggestions(false);
if (props.onItemClick) {
props.onItemClick(i);
}
}}
className={`autocomplete__items__item ${pos === n ? 'autocomplete__items__item--selected' : ''}`}>
{i}
</div>
))}
</ThemeDiv>
), document.body)}
</div>
);
};
Loading

0 comments on commit 9ebea33

Please sign in to comment.