From 61db41b3707e3462341eb2f6e08b5e4ca5fed05b Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Wed, 9 Aug 2023 19:55:10 +0000 Subject: [PATCH 1/8] Automatically reposition suggestions depending on available space --- .../InlineAutocomplete/InlineAutocomplete.tsx | 4 +-- .../_AutocompleteSuggestions.tsx | 32 +++++++++++++++---- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/drafts/InlineAutocomplete/InlineAutocomplete.tsx b/src/drafts/InlineAutocomplete/InlineAutocomplete.tsx index ddc287d7a81..f0123272ea1 100644 --- a/src/drafts/InlineAutocomplete/InlineAutocomplete.tsx +++ b/src/drafts/InlineAutocomplete/InlineAutocomplete.tsx @@ -108,7 +108,6 @@ const InlineAutocomplete = ({ (getSelectionStart(inputRef.current) ?? 0) - showEventRef.current.query.length, ) : {top: 0, left: 0, height: 0} - const suggestionsOffset = {top: triggerCharCoords.top + triggerCharCoords.height, left: triggerCharCoords.left} // User can blur while suggestions are visible with shift+tab const onBlur: React.FocusEventHandler = () => { @@ -198,8 +197,7 @@ const InlineAutocomplete = ({ inputRef={inputRef} onCommit={onCommit} onClose={onHideSuggestions} - top={suggestionsOffset.top || 0} - left={suggestionsOffset.left || 0} + triggerCharCoords={triggerCharCoords} visible={suggestionsVisible} tabInsertsSuggestions={tabInsertsSuggestions} /> diff --git a/src/drafts/InlineAutocomplete/_AutocompleteSuggestions.tsx b/src/drafts/InlineAutocomplete/_AutocompleteSuggestions.tsx index 4483757ba15..6990f20e62e 100644 --- a/src/drafts/InlineAutocomplete/_AutocompleteSuggestions.tsx +++ b/src/drafts/InlineAutocomplete/_AutocompleteSuggestions.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useState} from 'react' +import React, {useCallback, useLayoutEffect, useRef, useState} from 'react' import Spinner from '../../Spinner' import {ActionList, ActionListItemProps} from '../../ActionList' import Box from '../../Box' @@ -7,13 +7,12 @@ import Overlay from '../../Overlay' import {Suggestion, Suggestions, TextInputElement} from './types' import {getSuggestionKey, getSuggestionValue} from './utils' +import {CharacterCoordinates} from '../utils/character-coordinates' type AutoCompleteSuggestionsProps = { suggestions: Suggestions | null portalName?: string - // make top/left primitives instead of a Coordinates object to avoid extra re-renders - top: number - left: number + triggerCharCoords: CharacterCoordinates onClose: () => void onCommit: (suggestion: string) => void inputRef: React.RefObject @@ -54,14 +53,15 @@ const SuggestionListItem = ({suggestion}: {suggestion: Suggestion}) => { const AutocompleteSuggestions = ({ suggestions, portalName, - top, - left, + triggerCharCoords, onClose, onCommit: externalOnCommit, inputRef, visible, tabInsertsSuggestions, }: AutoCompleteSuggestionsProps) => { + const overlayRef = useRef(null) + // It seems wierd to use state instead of a ref here, but because the list is inside an // AnchoredOverlay it is not always mounted - so we want to reinitialize the Combobox when it mounts const [list, setList] = useState(null) @@ -85,6 +85,22 @@ const AutocompleteSuggestions = ({ defaultFirstOption: true, }) + const [top, setTop] = useState(0) + useLayoutEffect( + function reCalculateTop() { + const overlayHeight = overlayRef.current?.offsetHeight ?? 0 + + const yOffsetBottomSide = triggerCharCoords.top + triggerCharCoords.height + const wouldOverflowBottom = yOffsetBottomSide + overlayHeight > window.innerHeight + + const yOffsetTopSide = triggerCharCoords.top - overlayHeight + + setTop(wouldOverflowBottom ? yOffsetTopSide : yOffsetBottomSide) + }, + // this is a cheap effect and we want it to run when pretty much anything that could affect position changes + [triggerCharCoords.top, triggerCharCoords.height, suggestions, visible], + ) + // Conditional rendering appears wrong at first - it means that we are reconstructing the // Combobox instance every time the suggestions appear. But this is what we want - otherwise // the textarea would always have the `combobox` role, which is incorrect (a textarea should @@ -98,7 +114,9 @@ const AutocompleteSuggestions = ({ preventFocusOnOpen portalContainerName={portalName} sx={{position: 'fixed'}} - {...{top, left}} + top={top} + left={triggerCharCoords.left} + ref={overlayRef} > {suggestions === 'loading' ? ( From 95dd51c439abfcf6c94f63f4da3a011c0bb8cad5 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Wed, 9 Aug 2023 19:59:27 +0000 Subject: [PATCH 2/8] Use isomorphic layout effect --- src/drafts/InlineAutocomplete/_AutocompleteSuggestions.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/drafts/InlineAutocomplete/_AutocompleteSuggestions.tsx b/src/drafts/InlineAutocomplete/_AutocompleteSuggestions.tsx index 6990f20e62e..3492e07815c 100644 --- a/src/drafts/InlineAutocomplete/_AutocompleteSuggestions.tsx +++ b/src/drafts/InlineAutocomplete/_AutocompleteSuggestions.tsx @@ -8,6 +8,7 @@ import Overlay from '../../Overlay' import {Suggestion, Suggestions, TextInputElement} from './types' import {getSuggestionKey, getSuggestionValue} from './utils' import {CharacterCoordinates} from '../utils/character-coordinates' +import useIsomorphicLayoutEffect from '../../utils/useIsomorphicLayoutEffect' type AutoCompleteSuggestionsProps = { suggestions: Suggestions | null @@ -86,7 +87,7 @@ const AutocompleteSuggestions = ({ }) const [top, setTop] = useState(0) - useLayoutEffect( + useIsomorphicLayoutEffect( function reCalculateTop() { const overlayHeight = overlayRef.current?.offsetHeight ?? 0 From 8d451379f282ad81e8cfaf9c2841fb66b4ef6a0d Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Wed, 9 Aug 2023 16:06:52 -0400 Subject: [PATCH 3/8] Create old-cherries-smile.md --- .changeset/old-cherries-smile.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/old-cherries-smile.md diff --git a/.changeset/old-cherries-smile.md b/.changeset/old-cherries-smile.md new file mode 100644 index 00000000000..0f43cd49973 --- /dev/null +++ b/.changeset/old-cherries-smile.md @@ -0,0 +1,7 @@ +--- +"@primer/react": patch +--- + +Automatically reposition `InlineAutocomplete` suggestions depending on available space + + From 3f28aded3495135ae35563c2a0d20d186bcf5d06 Mon Sep 17 00:00:00 2001 From: Ian Sanders Date: Wed, 9 Aug 2023 20:42:02 +0000 Subject: [PATCH 4/8] Add story --- .../InlineAutocomplete.features.stories.tsx | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/drafts/InlineAutocomplete/InlineAutocomplete.features.stories.tsx b/src/drafts/InlineAutocomplete/InlineAutocomplete.features.stories.tsx index 2e467e5d3f8..8352d5e7913 100644 --- a/src/drafts/InlineAutocomplete/InlineAutocomplete.features.stories.tsx +++ b/src/drafts/InlineAutocomplete/InlineAutocomplete.features.stories.tsx @@ -105,3 +105,33 @@ export const CustomRendering = ({loading, tabInserts}: ArgProps) => { ) } + +export const AutoPositioning = () => { + const [suggestions, setSuggestions] = useState(null) + + const onShowSuggestions = (event: ShowSuggestionsEvent) => { + setSuggestions( + filteredUsers(event.query).map(user => ({ + value: user.login, + render: props => , + })), + ) + } + + const onHideSuggestions = () => setSuggestions(null) + + return ( + + Inline Autocomplete Demo + Try typing '@' to show user suggestions. + +