ui: prevent overflow in filter modals

This commit is contained in:
nick-delirium 2025-02-10 09:37:29 +01:00
parent 4f2b8d43b7
commit 06bad31a7d
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
2 changed files with 135 additions and 83 deletions

View file

@ -3,10 +3,15 @@ import { Button, Checkbox, Input, Tooltip } from 'antd';
import { RedoOutlined, CloseCircleFilled } from '@ant-design/icons';
import cn from 'classnames';
import { Loader } from 'UI';
import OutsideClickDetectingDiv from '../../OutsideClickDetectingDiv';
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
function TruncatedText({ text, maxWidth }: { text?: string; maxWidth?: string;}) {
function TruncatedText({
text,
maxWidth,
}: {
text?: string;
maxWidth?: string;
}) {
const textRef = useRef<HTMLDivElement>(null);
const [isTruncated, setIsTruncated] = useState(false);
@ -34,7 +39,6 @@ function TruncatedText({ text, maxWidth }: { text?: string; maxWidth?: string;})
);
}
export function AutocompleteModal({
onClose,
onApply,
@ -45,6 +49,7 @@ export function AutocompleteModal({
isLoading,
placeholder,
commaQuery,
isOpen,
}: {
values: string[];
onClose: () => void;
@ -55,7 +60,9 @@ export function AutocompleteModal({
placeholder?: string;
isLoading?: boolean;
commaQuery?: boolean;
isOpen?: boolean;
}) {
const modalRef = useRef<HTMLDivElement>(null);
const [query, setQuery] = React.useState('');
const [selectedValues, setSelectedValues] = React.useState<string[]>(
values.filter((i) => i && i.length > 0)
@ -118,79 +125,101 @@ export function AutocompleteModal({
return str;
}, [query]);
React.useEffect(() => {
if (modalRef.current) {
const modalRect = modalRef.current.getBoundingClientRect();
const viewportWidth = window.innerWidth;
if (modalRect.right > viewportWidth) {
modalRef.current.style.left = `unset`;
modalRef.current.style.right = `0`;
}
}
}, [isOpen]);
return (
<OutsideClickDetectingDiv
className={cn(
'absolute left-0 mt-2 p-4 bg-white rounded-xl shadow border-gray-light z-10'
)}
style={{ width: 360, minHeight: 100, top: '100%' }}
onClickOutside={() => {
onClose();
}}
>
<Input.Search
value={query}
onFocus={handleFocus}
loading={isLoading}
onChange={(e) => handleInputChange(e.target.value)}
placeholder={placeholder}
className="rounded-lg"
autoFocus
allowClear
/>
<Loader loading={isLoading}>
<>
<div
className={
'flex flex-col gap-2 overflow-y-auto py-2 overflow-x-hidden text-ellipsis'
}
style={{ maxHeight: 200 }}
>
{sortedOptions.map((item) => (
<div
key={item.value}
onClick={() => onSelectOption(item)}
className={
'cursor-pointer w-full py-1 hover:bg-active-blue rounded px-2'
}
>
<Checkbox checked={isSelected(item)} /> {item.label}
</div>
))}
</div>
{query.length ? (
<div className={'border-y border-y-gray-light py-2'}>
<div
className={
'whitespace-normal rounded cursor-pointer text-teal hover:bg-active-blue px-2 py-1'
}
onClick={applyQuery}
>
Apply {queryStr}
</div>
<div
className={cn(
'absolute left-0 mt-2 p-4 bg-white rounded-xl shadow border-gray-light z-10'
)}
ref={modalRef}
style={{ width: 360, minHeight: 100, top: '100%' }}
>
<Input.Search
value={query}
onFocus={handleFocus}
loading={isLoading}
onChange={(e) => handleInputChange(e.target.value)}
placeholder={placeholder}
className="rounded-lg"
autoFocus
allowClear
/>
<Loader loading={isLoading}>
<>
<div
className={
'flex flex-col gap-2 overflow-y-auto py-2 overflow-x-hidden text-ellipsis'
}
style={{ maxHeight: 200 }}
>
{sortedOptions.map((item) => (
<div
key={item.value}
onClick={() => onSelectOption(item)}
className={
'cursor-pointer w-full py-1 hover:bg-active-blue rounded px-2'
}
>
<Checkbox checked={isSelected(item)} /> {item.label}
</div>
))}
</div>
) : null}
</>
</Loader>
<div className="flex justify-between items-center pt-2">
<div className="flex gap-2 items-center">
<Button type="primary" onClick={applyValues} className="btn-apply-event-value">
Apply
</Button>
{query.length ? (
<div className={'border-y border-y-gray-light py-2'}>
<div
className={
'whitespace-normal rounded cursor-pointer text-teal hover:bg-active-blue px-2 py-1'
}
onClick={applyQuery}
>
Apply {queryStr}
</div>
</div>
) : null}
</>
</Loader>
<div className="flex justify-between items-center pt-2">
<div className="flex gap-2 items-center">
<Button
type="primary"
onClick={applyValues}
className="btn-apply-event-value"
>
Apply
</Button>
</div>
<Tooltip title="Clear all selection">
<Button
onClick={clearSelection}
type="text"
className="btn-clear-selection"
disabled={selectedValues.length === 0}
>
<RedoOutlined />
</Button>
</Tooltip>
</div>
<Tooltip title='Clear all selection'>
<Button onClick={clearSelection} type='text' className="btn-clear-selection" disabled={selectedValues.length === 0}>
<RedoOutlined />
</Button>
</Tooltip>
</div>
</OutsideClickDetectingDiv>
);
}
interface Props {
value: string[];
params?: any;
@ -201,7 +230,6 @@ interface Props {
mapValues?: (value: string) => string;
}
export function AutoCompleteContainer(props: Props) {
const filterValueContainer = useRef<HTMLDivElement>(null);
const [showValueModal, setShowValueModal] = useState(false);
@ -213,20 +241,19 @@ export function AutoCompleteContainer(props: Props) {
props.onApplyValues(values);
setShowValueModal(false);
}, 100);
console.log("closed on apply");
};
const onClearClick = (event: React.MouseEvent) => {
event.stopPropagation();
props.onApplyValues([]);
event.stopPropagation();
props.onApplyValues([]);
setShowValueModal(false);
console.log("closed clear click");
};
const handleContainerClick = (event: React.MouseEvent) => {
if (event.target === event.currentTarget ||
event.currentTarget.contains(event.target as Node)) {
if (
event.target === event.currentTarget ||
event.currentTarget.contains(event.target as Node)
) {
setShowValueModal(true);
}
};
@ -249,14 +276,22 @@ export function AutoCompleteContainer(props: Props) {
{!isEmpty ? (
<>
<TruncatedText
text={props.mapValues ? props.mapValues(props.value[0]) : props.value[0]}
text={
props.mapValues
? props.mapValues(props.value[0])
: props.value[0]
}
maxWidth="8rem"
/>
{props.value.length > 1 && (
<>
<span className="text-neutral-500/90">or</span>
<TruncatedText
text={props.mapValues ? props.mapValues(props.value[1]) : props.value[1]}
text={
props.mapValues
? props.mapValues(props.value[1])
: props.value[1]
}
maxWidth="8rem"
/>
{props.value.length > 2 && (
@ -274,13 +309,13 @@ export function AutoCompleteContainer(props: Props) {
</div>
)}
{!isEmpty && hovered && (
<div
className="absolute right-2 cursor-pointer flex items-center justify-center"
onClick={onClearClick}
>
<CloseCircleFilled className='text-neutral-200' />
</div>
)}
<div
className="absolute right-2 cursor-pointer flex items-center justify-center"
onClick={onClearClick}
>
<CloseCircleFilled className="text-neutral-200" />
</div>
)}
</div>
{showValueModal ? (
<props.modalRenderer
@ -289,8 +324,9 @@ export function AutoCompleteContainer(props: Props) {
onClose={onClose}
onApply={onApply}
values={props.value}
isOpen={showValueModal}
/>
) : null}
</div>
);
}
}

View file

@ -37,12 +37,25 @@ function FilterSelection(props: Props) {
isLive,
} = props;
const [showModal, setShowModal] = useState(false);
const modalRef = React.useRef<HTMLDivElement>(null);
const onAddFilter = (filter: any) => {
onFilterClick(filter);
setShowModal(false);
}
React.useEffect(() => {
if (showModal && modalRef.current) {
const modalRect = modalRef.current.getBoundingClientRect();
const viewportWidth = window.innerWidth;
if (modalRect.right > viewportWidth) {
modalRef.current.style.left = `unset`;
modalRef.current.style.right = `-280px`;
}
}
}, [showModal]);
const label = filter?.category === 'Issue' ? 'Issue' : filter?.label;
return (
<div className="relative flex-shrink-0 my-1.5">
@ -84,7 +97,10 @@ function FilterSelection(props: Props) {
</div>
)}
{showModal && (
<div className="absolute mt-2 left-0 rounded-2xl shadow-lg bg-white z-50">
<div
ref={modalRef}
className="absolute mt-2 left-0 rounded-2xl shadow-lg bg-white z-50"
>
<FilterModal
isLive={isRoute(ASSIST_ROUTE, window.location.pathname) || isLive}
onFilterClick={onAddFilter}