Skip to content

Commit

Permalink
Improved DropdownSelector component (#557)
Browse files Browse the repository at this point in the history
  • Loading branch information
bexsoft authored Oct 30, 2023
1 parent 096ffb1 commit 87ef467
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 64 deletions.
1 change: 1 addition & 0 deletions src/components/Autocomplete/Autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ const Autocomplete: FC<AutocompleteProps> = ({
}}
open={isOpen}
anchorEl={anchorEl}
useAnchorWidth
/>
</Box>
</InputContainer>
Expand Down
80 changes: 80 additions & 0 deletions src/components/DropdownSelector/DropdownSelector.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// This file is part of MinIO Design System
// Copyright (c) 2023 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

import React, { useState } from "react";
import { Meta, Story } from "@storybook/react";

import DropdownSelector from "./DropdownSelector";
import { DropdownSelectorProps } from "./DropdownSelector.types";

import StoryThemeProvider from "../../utils/StoryThemeProvider";
import GlobalStyles from "../GlobalStyles/GlobalStyles";
import Box from "../Box/Box";
import Button from "../Button/Button";
import TestIcon from "../../utils/TestIcon";

export default {
title: "MDS/Forms/DropdownSelector",
component: DropdownSelector,
argTypes: {},
} as Meta<typeof DropdownSelector>;

const Template: Story<DropdownSelectorProps> = (args) => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const openDownloadMenu = Boolean(anchorEl);

const handleCloseDownload = () => {
setAnchorEl(null);
};

return (
<StoryThemeProvider>
<GlobalStyles />
<Button
id={"test-button"}
onClick={(e) => {
setAnchorEl(e.currentTarget);
}}
icon={<TestIcon />}
/>
<DropdownSelector
open={openDownloadMenu}
anchorEl={anchorEl}
hideTriggerAction={() => {
handleCloseDownload();
}}
{...args}
/>
</StoryThemeProvider>
);
};

export const Default = Template.bind({});
Default.args = {
options: [
{ label: "Test Label 1", value: "tl1" },
{ label: "Test Label 2", value: "tl2" },
],
};

export const AnchorEnd = Template.bind({});
AnchorEnd.args = {
options: [
{ label: "Test Label 1", value: "tl1" },
{ label: "Test Label 2", value: "tl2" },
],
anchorOrigin: "end",
};
146 changes: 82 additions & 64 deletions src/components/DropdownSelector/DropdownSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,72 +36,78 @@ const SelectorContainer = styled.div(({}) => ({
overscrollBehavior: "contain",
}));

const DropdownBlock = styled.div<DropDownBlockProps>(({ theme, sx }) => ({
position: "absolute",
display: "flex",
backgroundColor: get(theme, "dropdownSelector.backgroundColor", "#fff"),
border: `1px solid ${get(theme, "borderColor", "#E2E2E2")}`,
padding: "10px 0",
maxHeight: 450,
minWidth: 150,
overflowY: "auto",
borderRadius: 4,
boxShadow:
"rgba(0, 0, 0, 0.2) 0px 11px 15px -7px, rgba(0, 0, 0, 0.14) 0px 24px 38px 3px, rgba(0, 0, 0, 0.12) 0px 9px 46px 8px",
"& ul": {
padding: 0,
margin: 0,
const DropdownBlock = styled.div<DropDownBlockProps>(
({ theme, sx, useAnchorWidth }) => ({
position: "absolute",
display: "flex",
flexDirection: "column",
width: "100%",
"& > li": {
cursor: "pointer",
listStyle: "none",
width: "100%",
color: get(theme, "dropdownSelector.optionTextColor", "#000"),
padding: "6px 15px",
fontSize: 14,
userSelect: "none",
backgroundColor: get(theme, "dropdownSelector.backgroundColor", "#fff"),
border: `1px solid ${get(theme, "borderColor", "#E2E2E2")}`,
padding: "10px 0",
maxHeight: 450,
minWidth: useAnchorWidth ? 150 : 0,
overflowY: "auto",
borderRadius: 4,
boxShadow:
"rgba(0, 0, 0, 0.2) 0px 11px 15px -7px, rgba(0, 0, 0, 0.14) 0px 24px 38px 3px, rgba(0, 0, 0, 0.12) 0px 9px 46px 8px",
"& ul": {
padding: 0,
margin: 0,
display: "flex",
alignItems: "center",
gap: 10,
"& svg": {
width: 16,
height: 16,
},
'&:not([class*="Mui"])::before': {
content: "' '",
},
"&.selected": {
backgroundColor: get(
theme,
"dropdownSelector.selectedBGColor",
"#D5D7D8",
),
flexDirection: "column",
width: "100%",
"& > li": {
cursor: "pointer",
listStyle: "none",
width: "100%",
color: get(theme, "dropdownSelector.optionTextColor", "#000"),
},
"&.disabled": {
cursor: "not-allowed",
color: get(theme, "dropdownSelector.disabledText", "#E6EBEB"),
"&:hover": {
padding: "6px 15px",
fontSize: 14,
userSelect: "none",
display: "flex",
alignItems: "center",
gap: 10,
"& svg": {
width: 16,
height: 16,
},
'&:not([class*="Mui"])::before': {
content: "' '",
},
"&.selected": {
backgroundColor: get(
theme,
"dropdownSelector.backgroundColor",
"#fff",
"dropdownSelector.selectedBGColor",
"#D5D7D8",
),
color: get(theme, "dropdownSelector.optionTextColor", "#000"),
},
"&.disabled": {
cursor: "not-allowed",
color: get(theme, "dropdownSelector.disabledText", "#E6EBEB"),
"&:hover": {
backgroundColor: get(
theme,
"dropdownSelector.backgroundColor",
"#fff",
),
color: get(theme, "dropdownSelector.disabledText", "#E6EBEB"),
},
},
"&.hovered:not(.disabled)": {
backgroundColor: get(theme, "dropdownSelector.hoverBG", "#E6EAEB"),
color: get(theme, "dropdownSelector.hoverText", "#000"),
},
},
"&.hovered:not(.disabled)": {
backgroundColor: get(theme, "dropdownSelector.hoverBG", "#E6EAEB"),
color: get(theme, "dropdownSelector.hoverText", "#000"),
},
},
},
...sx,
}));

const calcElementPosition = (anchorEl: (EventTarget & HTMLElement) | null) => {
...sx,
}),
);

const calcElementPosition = (
anchorEl: (EventTarget & HTMLElement) | null,
anchorOrigin: "start" | "end",
useAnchorWidth: boolean,
) => {
if (!anchorEl) {
return {
top: 0,
Expand All @@ -112,11 +118,21 @@ const calcElementPosition = (anchorEl: (EventTarget & HTMLElement) | null) => {

const bounds = anchorEl.getBoundingClientRect();

return {
top: bounds.top + bounds.height,
left: bounds.left,
width: bounds.width,
};
let returnItem: CSSObject = { top: bounds.top + bounds.height };

if (anchorOrigin === "start") {
returnItem.left = bounds.left;
returnItem.transform = "translateX(0%)";
} else if (anchorOrigin === "end") {
returnItem.left = bounds.left + bounds.width;
returnItem.transform = "translateX(-100%)";
}

if (useAnchorWidth) {
returnItem.width = bounds.width;
}

return returnItem;
};

const DropdownSelector: FC<DropdownSelectorProps> = ({
Expand All @@ -127,6 +143,8 @@ const DropdownSelector: FC<DropdownSelectorProps> = ({
hideTriggerAction,
open,
anchorEl = null,
useAnchorWidth = false,
anchorOrigin = "start",
}) => {
const [coords, setCoords] = useState<CSSObject | null>(null);
const [indexHover, setIndexHover] = useState<number>(0);
Expand Down Expand Up @@ -166,7 +184,7 @@ const DropdownSelector: FC<DropdownSelectorProps> = ({

useEffect(() => {
if (open) {
setCoords(calcElementPosition(anchorEl));
setCoords(calcElementPosition(anchorEl, anchorOrigin, useAnchorWidth));
return;
}
setCoords(null);
Expand All @@ -181,7 +199,7 @@ const DropdownSelector: FC<DropdownSelectorProps> = ({
if (!anchorEl || !anchorEl.getBoundingClientRect()) {
return;
}
setCoords(calcElementPosition(anchorEl));
setCoords(calcElementPosition(anchorEl, anchorOrigin, useAnchorWidth));
}, 300);

window.addEventListener("resize", handleResize);
Expand All @@ -202,7 +220,7 @@ const DropdownSelector: FC<DropdownSelectorProps> = ({

return createPortal(
<SelectorContainer onClick={hideTriggerAction}>
<DropdownBlock id={id} sx={coords}>
<DropdownBlock id={id} sx={coords} useAnchorWidth={useAnchorWidth}>
<ul>
{options.map((option, index) => {
return (
Expand Down
3 changes: 3 additions & 0 deletions src/components/DropdownSelector/DropdownSelector.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@ export interface DropdownSelectorProps {
hideTriggerAction: () => void;
open: boolean;
anchorEl?: (EventTarget & HTMLElement) | null;
anchorOrigin?: "start" | "end";
useAnchorWidth?: boolean;
}

export interface DropDownBlockProps {
useAnchorWidth: boolean;
sx: CSSObject;
}
1 change: 1 addition & 0 deletions src/components/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ const Select: FC<SelectProps> = ({
}}
open={isOpen}
anchorEl={anchorEl}
useAnchorWidth
/>
</Box>
</InputContainer>
Expand Down

0 comments on commit 87ef467

Please sign in to comment.