Skip to content

Commit

Permalink
chore(combobox): fix positioning (#3459)
Browse files Browse the repository at this point in the history
* chore(combobox): fix positioning

* chore: initialize open

* chore: final small fix

* fix: chromatic skip

* fix: random aria errors on sidebar fullcompositions
  • Loading branch information
TheSisb committed Aug 30, 2023
1 parent 10da7d2 commit 29b6b7b
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 25 deletions.
6 changes: 6 additions & 0 deletions .changeset/lucky-colts-grow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@twilio-paste/combobox': patch
'@twilio-paste/core': patch
---

[Combobox] Now opens the dropdown menu upwards when positioned towards the bottom of the viewport.
65 changes: 65 additions & 0 deletions packages/paste-core/components/combobox/src/ListboxPositioner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as React from 'react';
import {Box, type BoxStyleProps} from '@twilio-paste/box';
import {useRect} from '@radix-ui/react-use-rect';
import {useWindowSize} from '@twilio-paste/utils';

interface ListBoxPositionerProps {
children: React.ReactNode;
inputBoxRef: React.RefObject<HTMLElement>;
dropdownBoxRef: React.RefObject<HTMLElement>;
}

export const ListBoxPositioner: React.FC<ListBoxPositionerProps> = ({inputBoxRef, dropdownBoxRef, ...props}) => {
const {height: windowHeight} = useWindowSize();
const inputBoxDimensions = useRect(inputBoxRef.current);
const dropdownBoxDimensions = useRect(dropdownBoxRef.current);
const dropdownBoxHeight = dropdownBoxDimensions?.height;

const styles = React.useMemo((): BoxStyleProps => {
if (dropdownBoxHeight == null || inputBoxDimensions == null || dropdownBoxHeight === 0) {
return {};
}

/*
* Scenarios:
* 1- Dropdown height is bigger than window height
* - Then show at the top of the viewport
* 2- Dropdown height + inputbox bottom is bigger than viewport height
* - Show upwards
* 3- Dropdown height + inputbox bottom is smaller than viewport height
* - Show downwards
*/
if (windowHeight) {
if (dropdownBoxHeight >= windowHeight) {
return {
position: 'fixed',
top: 0,
left: inputBoxDimensions?.left,
right: inputBoxDimensions?.right,
width: inputBoxDimensions?.width,
};
}
if (dropdownBoxHeight + inputBoxDimensions?.bottom >= windowHeight) {
return {
position: 'fixed',
// 6px to account for border things, should be fine on all themes
top: inputBoxDimensions?.top - dropdownBoxHeight - 6,
left: inputBoxDimensions?.left,
right: inputBoxDimensions?.right,
width: inputBoxDimensions?.width,
};
}
}

return {
position: 'fixed',
top: inputBoxDimensions?.bottom,
left: inputBoxDimensions?.left,
right: inputBoxDimensions?.right,
width: inputBoxDimensions?.width,
};
}, [inputBoxDimensions, dropdownBoxHeight, windowHeight]);

return <Box {...props} {...styles} zIndex="zIndex90" />;
};
ListBoxPositioner.displayName = 'ListBoxPositioner';
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import {FormPillGroup, FormPill, useFormPillState} from '@twilio-paste/form-pill
import {useComboboxPrimitive, useMultiSelectPrimitive} from '@twilio-paste/combobox-primitive';
import {InputBox, InputChevronWrapper, getInputChevronIconColor} from '@twilio-paste/input-box';
import {Portal} from '@twilio-paste/reakit-library';
import {useRect} from '@radix-ui/react-use-rect';

import {ListBoxPositioner} from '../ListboxPositioner';
import {GrowingInput} from './GrowingInput';
import {ComboboxListbox} from '../styles/ComboboxListbox';
import {ComboboxItems} from '../ComboboxItems';
Expand Down Expand Up @@ -61,7 +61,6 @@ export const MultiselectCombobox = React.forwardRef<HTMLInputElement, Multiselec

// gets the dimensions of the inputBox to position the listbox
const inputBoxRef = React.useRef<HTMLDivElement>(null);
const inputBoxDimensions = useRect(inputBoxRef.current);

const onSelectedItemsChange = React.useCallback(
(changes: any) => {
Expand Down Expand Up @@ -363,14 +362,7 @@ export const MultiselectCombobox = React.forwardRef<HTMLInputElement, Multiselec
</Box>
</InputBox>
<Portal>
<Box
position="fixed"
top={inputBoxDimensions?.bottom}
left={inputBoxDimensions?.left}
right={inputBoxDimensions?.right}
width={inputBoxDimensions?.width}
zIndex="zIndex90"
>
<ListBoxPositioner inputBoxRef={inputBoxRef} dropdownBoxRef={parentRef}>
<ComboboxListbox
{...getMenuProps({ref: parentRef})}
element={`${element}_LISTBOX`}
Expand All @@ -393,7 +385,7 @@ export const MultiselectCombobox = React.forwardRef<HTMLInputElement, Multiselec
emptyState={emptyState}
/>
</ComboboxListbox>
</Box>
</ListBoxPositioner>
</Portal>
{helpText && (
<HelpText id={helpTextId} variant={getHelpTextVariant(variant, hasError)} element={`${element}_HELP_TEXT`}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import {HelpText} from '@twilio-paste/help-text';
import type {HelpTextVariants} from '@twilio-paste/help-text';
import type {InputVariants} from '@twilio-paste/input';
import {Portal} from '@twilio-paste/reakit-library';
import {useRect} from '@radix-ui/react-use-rect';

import {ComboboxInputSelect} from '../styles/ComboboxInputSelect';
import {ComboboxInputWrapper} from '../styles/ComboboxInputWrapper';
import {ComboboxListbox} from '../styles/ComboboxListbox';
import {ComboboxItems} from '../ComboboxItems';
import type {ComboboxProps} from '../types';
import {extractPropsFromState} from './extractPropsFromState';
import {ListBoxPositioner} from '../ListboxPositioner';

const getHelpTextVariant = (variant: InputVariants, hasError: boolean | undefined): HelpTextVariants => {
if (hasError && variant === 'inverse') {
Expand Down Expand Up @@ -75,7 +75,6 @@ const Combobox = React.forwardRef<HTMLInputElement, ComboboxProps>(

// gets the dimensions of the inputBox to position the listbox
const inputBoxRef = React.useRef<HTMLDivElement>(null);
const inputBoxDimensions = useRect(inputBoxRef.current);

const {
getComboboxProps,
Expand Down Expand Up @@ -183,15 +182,7 @@ const Combobox = React.forwardRef<HTMLInputElement, ComboboxProps>(
</ComboboxInputWrapper>
</InputBox>
<Portal>
<Box
position="fixed"
top={inputBoxDimensions?.bottom}
left={inputBoxDimensions?.left}
right={inputBoxDimensions?.right}
width={inputBoxDimensions?.width}
zIndex="zIndex90"
display={isOpen ? 'block' : 'none'}
>
<ListBoxPositioner inputBoxRef={inputBoxRef} dropdownBoxRef={parentRef}>
<ComboboxListbox hidden={!isOpen} element={`${element}_LISTBOX`} {...getMenuProps({ref: parentRef})}>
<ComboboxItems
ref={scrollToIndexRef}
Expand All @@ -208,7 +199,7 @@ const Combobox = React.forwardRef<HTMLInputElement, ComboboxProps>(
emptyState={emptyState}
/>
</ComboboxListbox>
</Box>
</ListBoxPositioner>
</Portal>
{helpText && (
<HelpText id={helpTextId} variant={getHelpTextVariant(variant, hasError)}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,40 @@ export const DefaultCombobox: StoryFn = () => {

DefaultCombobox.storyName = 'Combobox';

export const BottomOfScreen: StoryFn = () => {
return (
<>
<Box height="size50" width="100%" />
<Combobox
items={iconItems}
labelText="Choose a component:"
helpText="This is the help text"
optionTemplate={(item: IconItems) => (
<MediaObject verticalAlign="center">
{item.iconLeft ? (
<MediaFigure spacing="space20">
<InformationIcon decorative={false} size="sizeIcon20" title="information" />
</MediaFigure>
) : null}

<MediaBody>{item.label}</MediaBody>
{item.iconRight ? (
<MediaFigure spacing="space20">
<InformationIcon decorative={false} size="sizeIcon20" title="information" />
</MediaFigure>
) : null}
</MediaObject>
)}
itemToString={(item: IconItems) => (item ? String(item.label) : '')}
/>
</>
);
};
BottomOfScreen.storyName = 'Bottom of screen';
BottomOfScreen.parameters = {
chromatic: {disableSnapshot: true},
};

const ItemToString = ({name}: {name: string}): string => name;

export const VirtualizedCombobox: StoryFn = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,34 @@ export const MultiselectComboboxBasic = (): React.ReactNode => {
};
MultiselectComboboxBasic.storyName = 'Basic';

export const BottomOfScreen = (): React.ReactNode => {
const [inputValue, setInputValue] = React.useState('');
const filteredItems = React.useMemo(() => getFilteredItems(inputValue), [inputValue]);

return (
<>
<Box height="size50" width="100%" />
<MultiselectCombobox
labelText="Choose a Paste Component"
selectedItemsLabelText="Selected Paste components"
helpText="Paste components are the building blocks of your product UI."
items={filteredItems}
onInputValueChange={({inputValue: newInputValue = ''}) => {
setInputValue(newInputValue);
}}
onSelectedItemsChange={(selectedItems: string[]) => {
// eslint-disable-next-line no-console
console.log(selectedItems);
}}
/>
</>
);
};
BottomOfScreen.storyName = 'Bottom of screen';
BottomOfScreen.parameters = {
chromatic: {disableSnapshot: true},
};

/*
* Basic - Inverse
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const Console: React.FC<React.PropsWithChildren<{collapsed: boolean; setC
<Box minWidth="1200px">
{/* Can be placed anywhere - position fixed */}
<Sidebar
aria-label={id}
collapsed={collapsed}
variant="compact"
sidebarNavigationSkipLinkID={sidebarNavigationSkipLinkID}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const Flex: React.FC<React.PropsWithChildren<{collapsed: boolean; setColl
return (
<Box minWidth="1200px">
<Sidebar
aria-label={id}
sidebarNavigationSkipLinkID={sidebarNavigationSkipLinkID}
topbarSkipLinkID={topbarSkipLinkID}
mainContentSkipLinkID={mainContentSkipLinkID}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const Segment: React.FC<React.PropsWithChildren<{collapsed: boolean; setC
<Box minWidth="1200px">
{/* Can be placed anywhere - position fixed */}
<Sidebar
aria-label={id}
sidebarNavigationSkipLinkID={sidebarNavigationSkipLinkID}
topbarSkipLinkID={topbarSkipLinkID}
mainContentSkipLinkID={mainContentSkipLinkID}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const Docs: StoryFn = () => {
<Box minWidth="1200px">
{/* Can be placed anywhere - position fixed */}
<Sidebar
aria-label={id}
sidebarNavigationSkipLinkID={sidebarNavigationSkipLinkID}
topbarSkipLinkID={topbarSkipLinkID}
mainContentSkipLinkID={mainContentSkipLinkID}
Expand Down Expand Up @@ -107,8 +108,8 @@ export const Docs: StoryFn = () => {
{/* Must wrap content area */}
<SidebarPushContentWrapper collapsed={pushSidebarCollapsed} variant="compact">
<Topbar id={topbarSkipLinkID}>
<TopbarActions justify="start">
<InPageNavigation aria-label="Product" marginBottom="space0">
<TopbarActions justify="start" aria-label={topbarSkipLinkID}>
<InPageNavigation aria-label={`Product ${topbarSkipLinkID}`} marginBottom="space0">
<InPageNavigationItem href="#" currentPage>
Messaging
</InPageNavigationItem>
Expand Down

0 comments on commit 29b6b7b

Please sign in to comment.