From 518be335983d3cf09955d86ab83a6f6438b77be5 Mon Sep 17 00:00:00 2001 From: Masoud Amjadi Date: Tue, 19 Sep 2023 18:48:06 -0400 Subject: [PATCH 1/2] feat(autocomplete): add multi-column feature to the autocomplete component --- .../src/core/Autocomplete/GITHUB_LABELS.tsx | 82 +++++++++++++++++++ .../__snapshots__/index.test.tsx.snap | 2 +- .../src/core/Autocomplete/index.stories.tsx | 13 ++- .../src/core/Autocomplete/index.tsx | 60 +++++++++++++- .../components/src/core/Autocomplete/style.ts | 44 +++++++++- 5 files changed, 193 insertions(+), 8 deletions(-) create mode 100644 packages/components/src/core/Autocomplete/GITHUB_LABELS.tsx diff --git a/packages/components/src/core/Autocomplete/GITHUB_LABELS.tsx b/packages/components/src/core/Autocomplete/GITHUB_LABELS.tsx new file mode 100644 index 000000000..e5ffac1d5 --- /dev/null +++ b/packages/components/src/core/Autocomplete/GITHUB_LABELS.tsx @@ -0,0 +1,82 @@ +// (masoudmanson): The unit tests rely on the content in this file; do not alter it! + +import { DefaultAutocompleteOption } from "."; + +const COLUMN_ONE = "Column One"; +const COLUMN_TWO = "Column Two"; +const COLUMN_THREE = "Column Three"; + +export const GITHUB_LABELS: DefaultAutocompleteOption[] = [ + { + name: "Status: can't reproduce", + section: COLUMN_ONE, + }, + { + name: "Status: confirmed", + section: COLUMN_ONE, + }, + { + count: 3, + name: "Status: duplicate", + section: COLUMN_ONE, + }, + { + count: 5, + name: "Status: needs information", + section: COLUMN_ONE, + }, + { + details: "This will not be worked on", + name: "Status: wont do/fix", + section: COLUMN_ONE, + }, + { + details: "This is still in progress", + name: "Status: work in progress", + section: COLUMN_ONE, + }, + { + details: "This needs to be fixed!", + name: "Type: bug", + section: COLUMN_TWO, + }, + { + count: 4, + name: "Type: discussion", + section: COLUMN_TWO, + }, + { + name: "Type: documentation", + section: COLUMN_TWO, + }, + { + name: "Type: enhancement", + section: COLUMN_TWO, + }, + { + name: "Type: epic", + section: COLUMN_TWO, + }, + { + count: 23, + details: "This one is disabled.", + name: "Type: feature request", + section: COLUMN_TWO, + }, + { + name: "Type: question", + section: COLUMN_TWO, + }, + { + name: "Epic: Documentation", + section: COLUMN_THREE, + }, + { + name: "Epic: Dropdown Menu", + section: COLUMN_THREE, + }, + { + name: "Epic: Migration to v5", + section: COLUMN_THREE, + }, +]; diff --git a/packages/components/src/core/Autocomplete/__snapshots__/index.test.tsx.snap b/packages/components/src/core/Autocomplete/__snapshots__/index.test.tsx.snap index 19b7f858a..d3ebc53ff 100644 --- a/packages/components/src/core/Autocomplete/__snapshots__/index.test.tsx.snap +++ b/packages/components/src/core/Autocomplete/__snapshots__/index.test.tsx.snap @@ -5,7 +5,7 @@ exports[` Default story renders snapshot 1`] = ` style="margin: 16px 0px 0px 24px; width: 300px;" >
= Multiple extends @@ -129,6 +129,11 @@ const Autocomplete = ( export default { argTypes: { + columnWidth: { + control: { + type: "number", + }, + }, groupBy: { control: { labels: ["No group by", "Group by section names"], @@ -137,6 +142,9 @@ export default { mapping: groupByOptions, options: Object.keys(groupByOptions), }, + isMultiColumn: { + control: { type: "boolean" }, + }, keepSearchOnSelect: { control: { type: "boolean" }, }, @@ -146,9 +154,6 @@ export default { multiple: { control: { type: "boolean" }, }, - search: { - control: { type: "boolean" }, - }, }, component: Autocomplete, // (masoudmanson) For the purpose of storybook, the button is removed diff --git a/packages/components/src/core/Autocomplete/index.tsx b/packages/components/src/core/Autocomplete/index.tsx index b2c7b89b1..d528e1af7 100644 --- a/packages/components/src/core/Autocomplete/index.tsx +++ b/packages/components/src/core/Autocomplete/index.tsx @@ -5,8 +5,10 @@ import { AutocompleteRenderOptionState, InputAdornment, AutocompleteProps as MuiAutocompleteProps, + Popper, + PopperProps, } from "@mui/material"; -import React, { ReactNode, SyntheticEvent, useState } from "react"; +import React, { ReactNode, SyntheticEvent, useCallback, useState } from "react"; import { noop } from "src/common/utils"; import ButtonIcon from "../ButtonIcon"; import { IconProps } from "../Icon"; @@ -16,6 +18,7 @@ import { InputBaseWrapper, StyleProps, StyledAutocomplete, + StyledListBox, StyledMenuInputSearch, StyledMenuItemDetails, StyledMenuItemText, @@ -58,6 +61,8 @@ interface ExtraAutocompleteProps extends StyleProps { InputBaseProps?: Partial; label: string; PaperComponent?: typeof StyledPaper | RenderFunctionType; + isMultiColumn?: boolean; + columnWidth?: number; } type CustomAutocompleteProps< @@ -102,10 +107,61 @@ const Autocomplete = < renderOption = defaultRenderOption, renderTags = defaultRenderTags, search = false, + isMultiColumn = false, } = props; const [inputValue, setInputValue] = useState(""); + // (masoudmanson): Utilizing useMemo hook to encapsulate the custom listbox component, + // mitigates the "Jump to Start" scroll glitch and enhances performance. + const MultiColumnListbox = React.useMemo( + () => + React.forwardRef< + HTMLUListElement, + React.HTMLAttributes + >((listboxProps, ref) => ), + [] + ); + + // (masoudmanson): As we required a wider popper for multi-column dropdowns, + // we enforced an 'auto' width in the style file to the popper component. unfortunately + // the default popperOffset can not calculate the popper position with a auto width. + // To achieve the desired placement, we employ popper modifiers to compute the new + // popper position on the page, relying on the reference element's coordinates. + + const DefaultPopperComponent = useCallback((popperProps: PopperProps) => { + return ( + { + const offsetLeft = params.state.rects.reference.x; + const offsetTop = + params.state.rects.reference.y + + params.state.rects.reference.height + + 8; + + params.state.elements.popper.style.transform = `translate3d(${offsetLeft}px, ${offsetTop}px, 0)`; + }, + name: "resize", + phase: "afterWrite", + }, + ]} + placement="bottom-start" + /> + ); + }, []); + return ( { - const { search, hasSections } = props; + const { search, hasSections, isMultiColumn } = props; const spacings = getSpaces(props); const colors = getColors(props); const borders = getBorders(props); @@ -54,6 +58,10 @@ export const StyledAutocomplete = styled(Autocomplete, { } } + & + .MuiAutocomplete-popper { + ${isMultiColumn ? `width: auto !important;` : ""} + } + & + .MuiAutocomplete-popper > .MuiAutocomplete-paper { ${ @@ -68,6 +76,8 @@ export const StyledAutocomplete = styled(Autocomplete, { padding-bottom: 0; padding-right: ${spacings?.s}px; + ${isMultiColumn ? isMultiColumnStyles(props) : null} + .MuiAutocomplete-option { min-height: unset; } @@ -82,6 +92,9 @@ export const StyledAutocomplete = styled(Autocomplete, { .MuiAutocomplete-option[aria-disabled="true"] { opacity: 1; + .menuItem-details { + color: ${colors?.gray[300]}; + } } .MuiAutocomplete-option[aria-selected="true"].Mui-focused { @@ -104,7 +117,7 @@ export const StyledAutocomplete = styled(Autocomplete, { margin-bottom: ${spacings?.m}px; position: relative; padding: 0 0 ${spacings?.xs}px 0 0; - border-bottom: ${borders?.gray[200]}; + border-bottom: ${isMultiColumn ? "none" : borders?.gray[200]}; & li:last-of-type { position: relative; @@ -115,6 +128,29 @@ export const StyledAutocomplete = styled(Autocomplete, { }} ` as typeof Autocomplete; +const isMultiColumnStyles = (props: StyleProps): string => { + const { columnWidth = 200 } = props; + const spacings = getSpaces(props); + const borders = getBorders(props); + + return ` + & > li { + overflow: scroll; + width: ${columnWidth >= 200 ? columnWidth : 200}px; + + border-right: ${borders?.gray[200]}; + margin-right: ${spacings?.s}px; + padding-right: ${spacings?.s}px; + + &:last-child { + border-right: none; + margin-right: 0; + padding-right: 0; + } + } + `; +}; + export const InputBaseWrapper = styled("div", { shouldForwardProp: (prop: string) => !doNotForwardProps.includes(prop), })` @@ -185,3 +221,7 @@ export const StyledMenuItemText = styled("div")` display: flex; flex-direction: column; `; + +export const StyledListBox = styled("ul")` + display: flex; +`; From 17feb67b776545eca263649fe59f818295e46df8 Mon Sep 17 00:00:00 2001 From: Masoud Amjadi Date: Mon, 25 Sep 2023 17:07:08 -0400 Subject: [PATCH 2/2] fix(snapshots): update test snapshots --- .../src/core/Autocomplete/__snapshots__/index.test.tsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/core/Autocomplete/__snapshots__/index.test.tsx.snap b/packages/components/src/core/Autocomplete/__snapshots__/index.test.tsx.snap index 684b50828..2a6207512 100644 --- a/packages/components/src/core/Autocomplete/__snapshots__/index.test.tsx.snap +++ b/packages/components/src/core/Autocomplete/__snapshots__/index.test.tsx.snap @@ -5,7 +5,7 @@ exports[` Default story renders snapshot 1`] = ` style="margin: 16px 0px 0px 24px; width: 300px;" >