From 9a361d21d5a002678d24e9521401ec4f6b9f8ffa Mon Sep 17 00:00:00 2001 From: Red J Adaya Date: Fri, 6 Sep 2024 05:38:36 +0800 Subject: [PATCH] responsive typeahead modal (#324) --- frontend/app/element/input.less | 4 +- frontend/app/modals/typeaheadmodal.less | 142 ++++++++++---------- frontend/app/modals/typeaheadmodal.tsx | 169 ++++++++++++++---------- frontend/app/view/preview/preview.tsx | 1 - 4 files changed, 168 insertions(+), 148 deletions(-) diff --git a/frontend/app/element/input.less b/frontend/app/element/input.less index c637b213d..7cd13ab29 100644 --- a/frontend/app/element/input.less +++ b/frontend/app/element/input.less @@ -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); diff --git a/frontend/app/modals/typeaheadmodal.less b/frontend/app/modals/typeaheadmodal.less index d5832aa55..2ee247f43 100644 --- a/frontend/app/modals/typeaheadmodal.less +++ b/frontend/app/modals/typeaheadmodal.less @@ -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; } } } diff --git a/frontend/app/modals/typeaheadmodal.tsx b/frontend/app/modals/typeaheadmodal.tsx index 673a34d98..6865aefb4 100644 --- a/frontend/app/modals/typeaheadmodal.tsx +++ b/frontend/app/modals/typeaheadmodal.tsx @@ -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(null); const inputRef = useRef(null); const realInputRef = useRef(null); + const suggestionsWrapperRef = useRef(null); const suggestionsRef = useRef(null); - const [suggestionsHeight, setSuggestionsHeight] = useState(undefined); - const [modalHeight, setModalHeight] = useState(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) =>
; @@ -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 = () => (
{renderBackdrop(onClickBackdrop)}
0 })} > -
- - - - ), - }} - /> -
0 ? "8px" : "0", - height: suggestionsHeight, - overflowY: "auto", - }} - > - {suggestions && ( - - )} -
+ + + + ), + }} + /> +
0 ? "8px" : "0", + overflowY: "auto", + }} + > + {suggestions?.length > 0 && ( + + )}
diff --git a/frontend/app/view/preview/preview.tsx b/frontend/app/view/preview/preview.tsx index 6b73f3484..2b77b92a7 100644 --- a/frontend/app/view/preview/preview.tsx +++ b/frontend/app/view/preview/preview.tsx @@ -930,7 +930,6 @@ const OpenFileModal = React.memo( return (