responsive typeahead modal (#324)

This commit is contained in:
Red J Adaya 2024-09-06 05:38:36 +08:00 committed by GitHub
parent 8ad84fd78a
commit 9a361d21d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 168 additions and 148 deletions

View file

@ -6,8 +6,8 @@
align-items: center;
border-radius: 6px;
position: relative;
min-height: 32px;
min-width: 100px;
min-height: 24px;
min-width: 50px;
width: 100%;
gap: 6px;
border: 2px solid var(--form-element-border-color);

View file

@ -4,7 +4,7 @@
@import "../mixins.less";
.type-ahead-modal-backdrop {
position: fixed;
position: absolute;
top: 0;
left: 0;
right: 0;
@ -14,94 +14,90 @@
}
.type-ahead-modal {
position: fixed;
position: absolute;
z-index: var(--zindex-typeahead-modal);
display: flex;
flex-direction: column;
align-items: flex-start;
border-radius: 8px;
border-radius: 6px;
border: 1px solid var(--modal-border-color);
background: var(--modal-bg-color);
box-shadow: 0px 13px 16px 0px rgba(0, 0, 0, 0.4);
padding: 6px;
flex-direction: column;
.content-wrapper {
display: flex;
width: 100%;
padding: 6px;
flex-direction: column;
align-items: center;
.label {
opacity: 0.5;
font-size: 13px;
white-space: nowrap;
}
&.has-suggestions {
gap: 6px;
}
.input {
border: none;
border-bottom: none;
height: 24px;
border-radius: 0;
.label {
opacity: 0.5;
font-size: 13px;
white-space: nowrap;
}
.input {
border: none;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
height: 24px;
border-radius: 0;
input {
width: 100%;
flex-shrink: 0;
padding: 4px 6px;
}
.input-decoration.end-position {
margin: 6px;
i {
opacity: 0.3;
}
}
}
.suggestions-wrapper {
input {
width: 100%;
overflow-y: auto;
overflow-x: hidden;
display: flex;
flex-direction: column;
gap: 10px;
flex-shrink: 0;
padding: 4px 6px;
height: 24px;
}
.suggestion-header {
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 12px;
opacity: 0.7;
letter-spacing: 0.11px;
padding: 4px 0px 0px 4px;
.input-decoration.end-position {
margin: 6px;
i {
opacity: 0.3;
}
}
}
&.has-suggestions {
.input {
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
}
.suggestions-wrapper {
width: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
gap: 10px;
.suggestion-header {
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 12px;
opacity: 0.7;
letter-spacing: 0.11px;
padding: 4px 0px 0px 4px;
}
.suggestion-item {
width: 100%;
cursor: pointer;
display: flex;
padding: 6px 8px;
align-items: center;
gap: 8px;
align-self: stretch;
&:hover {
background-color: var(--highlight-bg-color);
border-radius: 4px;
}
.suggestion-item {
width: 100%;
cursor: pointer;
.name {
.ellipsis();
display: flex;
padding: 8px 6px;
align-items: center;
gap: 8px;
align-self: stretch;
&:hover {
background-color: var(--highlight-bg-color);
border-radius: 4px;
}
.name {
.ellipsis();
display: flex;
gap: 8px;
font-size: 11px;
font-weight: 400;
line-height: 14px;
}
font-size: 11px;
font-weight: 400;
line-height: 14px;
}
}
}

View file

@ -3,7 +3,7 @@ import { InputDecoration } from "@/app/element/inputdecoration";
import { useDimensions } from "@/app/hook/useDimensions";
import { makeIconClass } from "@/util/util";
import clsx from "clsx";
import React, { forwardRef, useEffect, useLayoutEffect, useRef, useState } from "react";
import React, { forwardRef, useLayoutEffect, useRef } from "react";
import ReactDOM from "react-dom";
import "./typeaheadmodal.less";
@ -12,13 +12,9 @@ type ConnStatus = "connected" | "connecting" | "disconnected" | "error";
interface BaseItem {
label: string;
value: string;
icon?: string | React.ReactNode;
}
interface FileItem extends BaseItem {
value: string;
}
interface ConnectionItem extends BaseItem {
status: ConnStatus;
iconColor: string;
@ -29,7 +25,7 @@ interface ConnectionScope {
items: ConnectionItem[];
}
type SuggestionsType = FileItem | ConnectionItem | ConnectionScope;
type SuggestionsType = ConnectionItem | ConnectionScope;
interface SuggestionsProps {
suggestions?: SuggestionsType[];
@ -104,32 +100,74 @@ const TypeAheadModal = ({
const modalRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLDivElement>(null);
const realInputRef = useRef<HTMLInputElement>(null);
const suggestionsWrapperRef = useRef<HTMLDivElement>(null);
const suggestionsRef = useRef<HTMLDivElement>(null);
const [suggestionsHeight, setSuggestionsHeight] = useState<number | undefined>(undefined);
const [modalHeight, setModalHeight] = useState<string | undefined>(undefined);
useEffect(() => {
if (modalRef.current && inputRef.current && suggestionsRef.current) {
const modalPadding = 32;
const inputHeight = inputRef.current.getBoundingClientRect().height;
let suggestionsTotalHeight = 0;
useLayoutEffect(() => {
if (modalRef.current || inputRef.current || suggestionsRef.current || suggestionsWrapperRef.current) return;
const suggestionItems = suggestionsRef.current.children;
for (let i = 0; i < suggestionItems.length; i++) {
suggestionsTotalHeight += suggestionItems[i].getBoundingClientRect().height;
}
const modalStyles = window.getComputedStyle(modalRef.current);
const paddingTop = parseFloat(modalStyles.paddingTop) || 0;
const paddingBottom = parseFloat(modalStyles.paddingBottom) || 0;
const borderTop = parseFloat(modalStyles.borderTopWidth) || 0;
const borderBottom = parseFloat(modalStyles.borderBottomWidth) || 0;
const modalPadding = paddingTop + paddingBottom;
const modalBorder = borderTop + borderBottom;
const totalHeight = modalPadding + inputHeight + suggestionsTotalHeight;
const maxHeight = height * 0.8;
const computedHeight = totalHeight > maxHeight ? maxHeight : totalHeight;
const suggestionsWrapperStyles = window.getComputedStyle(suggestionsWrapperRef.current);
const suggestionsWrapperMarginTop = parseFloat(suggestionsWrapperStyles.marginTop) || 0;
setModalHeight(`${computedHeight}px`);
const inputHeight = inputRef.current.getBoundingClientRect().height;
let suggestionsTotalHeight = 0;
const padding = 16 * 2;
setSuggestionsHeight(computedHeight - inputHeight - padding);
const suggestionItems = suggestionsRef.current.children;
for (let i = 0; i < suggestionItems.length; i++) {
suggestionsTotalHeight += suggestionItems[i].getBoundingClientRect().height;
}
const totalHeight =
modalPadding + modalBorder + inputHeight + suggestionsTotalHeight + suggestionsWrapperMarginTop;
const maxHeight = height * 0.8;
const computedHeight = totalHeight > maxHeight ? maxHeight : totalHeight;
modalRef.current.style.height = `${computedHeight}px`;
suggestionsWrapperRef.current.style.height = `${computedHeight - inputHeight - modalPadding - modalBorder - suggestionsWrapperMarginTop}px`;
}, [height, suggestions]);
useLayoutEffect(() => {
if (!blockRef.current || !modalRef.current) return;
const blockRect = blockRef.current.getBoundingClientRect();
const anchorRect = anchorRef.current.getBoundingClientRect();
const minGap = 20;
const availableWidth = blockRect.width - minGap * 2;
let modalWidth = 300;
if (modalWidth > availableWidth) {
console.log("got here!!!!!");
modalWidth = availableWidth;
}
let leftPosition = anchorRect.left - blockRect.left;
const modalRightEdge = leftPosition + modalWidth;
const blockRightEdge = blockRect.width - (minGap - 4);
if (modalRightEdge > blockRightEdge) {
leftPosition -= modalRightEdge - blockRightEdge;
}
if (leftPosition < minGap) {
leftPosition = minGap;
}
modalRef.current.style.width = `${modalWidth}px`;
modalRef.current.style.left = `${leftPosition}px`;
}, [width]);
useLayoutEffect(() => {
if (giveFocusRef) {
giveFocusRef.current = () => {
@ -142,7 +180,14 @@ const TypeAheadModal = ({
giveFocusRef.current = null;
}
};
}, [giveFocusRef]);
}, []);
useLayoutEffect(() => {
if (anchorRef.current && modalRef.current) {
const parentElement = anchorRef.current.closest(".block-frame-default-header");
modalRef.current.style.top = `${parentElement?.getBoundingClientRect().height}px`;
}
}, []);
const renderBackdrop = (onClick) => <div className="type-ahead-modal-backdrop" onClick={onClick}></div>;
@ -158,59 +203,39 @@ const TypeAheadModal = ({
onSelect && onSelect(value);
};
let modalWidth = 300;
if (modalWidth < 300) {
modalWidth = Math.min(300, width * 0.95);
}
const anchorRect = anchorRef.current.getBoundingClientRect();
const blockRect = blockRef.current.getBoundingClientRect();
// Calculate positions relative to the wrapper
const topPosition = 30; // Adjusting the modal to be just below the anchor
const leftPosition = anchorRect.left - blockRect.left; // Relative left position to the wrapper div
const renderModal = () => (
<div className="type-ahead-modal-wrapper" onKeyDown={handleKeyDown}>
{renderBackdrop(onClickBackdrop)}
<div
ref={modalRef}
className={clsx("type-ahead-modal", className)}
style={{
top: topPosition,
left: leftPosition,
width: modalWidth,
maxHeight: modalHeight,
}}
className={clsx("type-ahead-modal", className, { "has-suggestions": suggestions?.length > 0 })}
>
<div className={clsx("content-wrapper", { "has-suggestions": suggestions?.length })}>
<Input
ref={inputRef}
inputRef={realInputRef}
onChange={handleChange}
value={value}
autoFocus={autoFocus}
placeholder={label}
decoration={{
endDecoration: (
<InputDecoration>
<i className="fa-regular fa-magnifying-glass"></i>
</InputDecoration>
),
}}
/>
<div
className="suggestions-wrapper"
style={{
marginTop: suggestions?.length > 0 ? "8px" : "0",
height: suggestionsHeight,
overflowY: "auto",
}}
>
{suggestions && (
<Suggestions ref={suggestionsRef} suggestions={suggestions} onSelect={handleSelect} />
)}
</div>
<Input
ref={inputRef}
inputRef={realInputRef}
onChange={handleChange}
value={value}
autoFocus={autoFocus}
placeholder={label}
decoration={{
endDecoration: (
<InputDecoration>
<i className="fa-regular fa-magnifying-glass"></i>
</InputDecoration>
),
}}
/>
<div
ref={suggestionsWrapperRef}
className="suggestions-wrapper"
style={{
marginTop: suggestions?.length > 0 ? "8px" : "0",
overflowY: "auto",
}}
>
{suggestions?.length > 0 && (
<Suggestions ref={suggestionsRef} suggestions={suggestions} onSelect={handleSelect} />
)}
</div>
</div>
</div>

View file

@ -930,7 +930,6 @@ const OpenFileModal = React.memo(
return (
<TypeAheadModal
label="Open path"
suggestions={[]}
blockRef={blockRef}
anchorRef={model.previewTextRef}
onKeyDown={handleKeyDown}