Skip to content

Commit

Permalink
feat: treeSelect uikit and PoolsTypeFilter widget (#10136)
Browse files Browse the repository at this point in the history
<!--
Before opening a pull request, please read the [contributing
guidelines](https://github.com/pancakeswap/pancake-frontend/blob/develop/CONTRIBUTING.md)
first
-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new `TreeSelect` component in the
`@pancakeswap/uikit` package. It also adds the `PoolsTypeFilter`
component to filter pool types in the FarmWidget.

### Detailed summary
- Added `TreeSelect` component to `@pancakeswap/uikit`
- Introduced `PoolsTypeFilter` component for filtering pool types in
FarmWidget
- Updated `MultiSelect` component UI and functionality

> The following files were skipped due to too many changes:
`packages/uikit/src/components/MultiSelect/assets/empty-message.svg`,
`packages/uikit/src/components/TreeSelect/TreeSelect.tsx`,
`packages/uikit/src/components/MultiSelect/MultiSelect.tsx`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your
question}`

<!-- end pr-codex -->
  • Loading branch information
chef-eric authored Jul 5, 2024
1 parent bfb64f1 commit 42dfc77
Show file tree
Hide file tree
Showing 14 changed files with 619 additions and 65 deletions.
6 changes: 6 additions & 0 deletions .changeset/tasty-meals-repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@pancakeswap/widgets-internal': minor
'@pancakeswap/uikit': minor
---

new UIKit: TreeSelect
16 changes: 16 additions & 0 deletions packages/uikit/src/components/MultiSelect/EmptyMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { styled } from "styled-components";
import { Image } from "../Image";
import emptyMessageIcon from "./assets/empty-message.svg";

const EmptyTips = styled.div`
font-size: 14px;
line-height: 21px;
text-align: center;
margin: 10px;
`;
export const EmptyMessage = ({ msg }: { msg?: string }) => (
<>
<Image src={emptyMessageIcon} width={80} height={80} />
<EmptyTips>{msg ?? "No data"}</EmptyTips>
</>
);
122 changes: 87 additions & 35 deletions packages/uikit/src/components/MultiSelect/MultiSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Checkbox } from "../Checkbox";
import { Column } from "../Column";
import { BORDER_RADIUS, IAdaptiveInputForwardProps, SearchBox } from "./SearchBox";
import { IMultiSelectProps, ISelectItem } from "./types";
import { EmptyMessage } from "./EmptyMessage";

const CHECKBOX_WIDTH = "26px";

Expand All @@ -26,7 +27,7 @@ const SelectContainer = styled.div`
.p-multiselect-panel {
min-width: auto;
border: 1px solid ${({ theme }) => theme.colors.inputSecondary};
border: 1px solid ${({ theme }) => theme.colors.cardBorder};
box-shadow: 0 0 3px ${({ theme }) => theme.shadows.inset};
border-radius: ${BORDER_RADIUS};
}
Expand All @@ -39,6 +40,11 @@ const SelectContainer = styled.div`
}
.p-multiselect-empty-message {
display: flex;
height: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
list-style-type: none;
text-align: center;
padding: 10px;
Expand All @@ -55,6 +61,14 @@ const SelectContainer = styled.div`
}
}
.p-multiselect-footer {
padding: 8px 16px;
text-align: center;
line-height: 24px;
font-size: 12px;
border-top: 1px solid var(--colors-cardBorder);
}
.p-checkbox {
position: relative;
display: inline-flex;
Expand All @@ -69,13 +83,13 @@ const SelectContainer = styled.div`
width: ${CHECKBOX_WIDTH};
height: ${CHECKBOX_WIDTH};
border-radius: 8px;
background-color: ${({ theme }) => theme.card.background};
background-color: ${({ theme }) => theme.colors.input};
}
&.p-highlight {
.p-checkbox-box {
border-color: ${({ theme }) => theme.colors.primary};
background-color: ${({ theme }) => theme.colors.primary};
border-color: ${({ theme }) => theme.colors.success};
background-color: ${({ theme }) => theme.colors.success};
}
.p-checkbox-icon.p-icon {
Expand All @@ -85,10 +99,10 @@ const SelectContainer = styled.div`
right: 0;
margin: auto;
transform: translateY(-50%);
color: ${({ theme }) => theme.colors.white};
color: ${({ theme }) => theme.colors.backgroundAlt};
path {
stroke: ${({ theme }) => theme.colors.white};
stroke: ${({ theme }) => theme.colors.backgroundAlt};
stroke-width: 1;
}
}
Expand All @@ -112,6 +126,16 @@ const SelectContainer = styled.div`
}
`;

const PrimereactSelectContainer = styled.div<{ scrollHeight?: string }>`
.p-multiselect-items-wrapper {
border-top: 1px solid var(--colors-cardBorder);
height: ${({ scrollHeight }) => scrollHeight ?? "auto"};
}
.p-multiselect-items {
height: 100%;
}
`;

const ItemIcon = styled.img`
width: 24px;
height: 24px;
Expand Down Expand Up @@ -201,22 +225,38 @@ export const MultiSelect = (props: IMultiSelectProps) => {
}
}, [selectAll, options]);

const handleFilter = (text: string) => {
const handleFilter = useCallback((text: string) => {
setSearchText(text);
};
}, []);

const handleLabelDelete = useCallback(
(item: ISelectItem) => {
if (!selectedItems?.length) {
return;
}
setSelectedItems(selectedItems.filter((i) => i !== item.value));
},
[selectedItems]
);

const panelHeaderTemplate = useMemo(
() => (
<>
{isFilter && <SearchBox selectedItems={selectedOptions} ref={searchInputRef} onFilter={handleFilter} />}
{isFilter && (
<SearchBox
selectedItems={selectedOptions}
ref={searchInputRef}
onFilter={handleFilter}
handleLabelDelete={handleLabelDelete}
/>
)}
{isSelectAll && (
<Box className="p-multiselect-item" onClick={handleSelectAll}>
<span>{selectAllLabel ?? "Select All"}</span>
<Checkbox
scale={CHECKBOX_WIDTH}
colors={{
background: "backgroundAlt",
checkedBackground: "primary",
background: "input",
border: "inputSecondary",
}}
style={{ margin: 0 }}
Expand All @@ -228,7 +268,17 @@ export const MultiSelect = (props: IMultiSelectProps) => {
)}
</>
),
[handleSelectAll, indeterminate, selectAll, selectAllLabel, selectedOptions, isFilter, isSelectAll]
[
handleSelectAll,
indeterminate,
selectAll,
selectAllLabel,
selectedOptions,
isFilter,
isSelectAll,
handleFilter,
handleLabelDelete,
]
);

const handleChange = useCallback(
Expand Down Expand Up @@ -290,29 +340,31 @@ export const MultiSelect = (props: IMultiSelectProps) => {
</Column>
</SelectInputContainer>

<PrimereactSelect
{...props}
ref={primereactSelectRef}
style={{
width: style?.width ?? "auto",
}}
panelStyle={{
...(panelStyle ?? {}),
width: panelStyle?.width ?? style?.width ?? "auto",
backgroundColor: panelStyle?.backgroundColor ?? theme.theme.colors.background,
}}
appendTo={props.appendTo ?? "self"}
value={value ?? selectedItems}
options={list}
placeholder={placeholder ?? "Select Something"}
itemTemplate={props.itemTemplate ?? itemTemplate}
panelHeaderTemplate={props.panelHeaderTemplate ?? panelHeaderTemplate}
emptyMessage={props.emptyMessage ?? "No data"}
onChange={handleChange}
onShow={handleShow}
onHide={handleHide}
onKeyDown={() => {}}
/>
<PrimereactSelectContainer scrollHeight={props.scrollHeight}>
<PrimereactSelect
{...props}
ref={primereactSelectRef}
style={{
width: style?.width ?? "auto",
}}
panelStyle={{
...(panelStyle ?? {}),
width: panelStyle?.width ?? style?.width ?? "auto",
backgroundColor: panelStyle?.backgroundColor ?? theme.theme.card.background,
}}
appendTo={props.appendTo ?? "self"}
value={value ?? selectedItems}
options={list}
placeholder={placeholder ?? "Select Something"}
itemTemplate={props.itemTemplate ?? itemTemplate}
panelHeaderTemplate={props.panelHeaderTemplate ?? panelHeaderTemplate}
emptyMessage={props.emptyMessage ?? ((<EmptyMessage />) as unknown as string)}
onChange={handleChange}
onShow={handleShow}
onHide={handleHide}
onKeyDown={() => {}}
/>
</PrimereactSelectContainer>
</SelectContainer>
);
};
80 changes: 54 additions & 26 deletions packages/uikit/src/components/MultiSelect/SearchBox.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
import { styled } from "styled-components";
import { IOptionType } from "./types";
import { useTheme } from "@pancakeswap/hooks";
import { IOptionType, ISelectItem } from "./types";
import { Box } from "../Box";
import { CrossIcon } from "../Svg";

export const BORDER_RADIUS = "16px";

const StyledBox = styled(Box)`
display: flex;
align-items: center;
flex-wrap: wrap;
border: 1px solid ${({ theme }) => theme.colors.inputSecondary};
background-color: ${({ theme }) => theme.colors.input};
border-radius: ${BORDER_RADIUS};
line-height: 24px;
margin: 16px;
Expand Down Expand Up @@ -77,38 +83,60 @@ const AdaptiveInput = forwardRef<IAdaptiveInputForwardProps, IAdaptiveInputProps
});

const SelectedLabel = styled.span`
display: inline-block;
margin: 2px;
padding: 2px 8px;
display: inline-flex;
padding: 2px;
border-radius: ${BORDER_RADIUS};
border: 1px solid ${({ theme }) => theme.colors.inputSecondary};
background-color: ${({ theme }) => theme.colors.textSubtle};
color: ${({ theme }) => theme.colors.background};
color: ${({ theme }) => theme.card.background};
gap: 4px;
`;

const ItemIcon = styled.img`
width: 24px;
height: 24px;
`;

export interface ISearchBoxProps {
selectedItems: IOptionType;
onFilter?: (text: string) => void;
handleLabelDelete: (item: ISelectItem) => void;
}

export const SearchBox = forwardRef<IAdaptiveInputForwardProps, ISearchBoxProps>(({ onFilter, selectedItems }, ref) => {
const inputRef = useRef<IAdaptiveInputForwardProps>(null);

useImperativeHandle(ref, () => ({
clear() {
inputRef.current?.clear();
},
focus() {
inputRef.current?.focus();
},
}));

return (
<StyledBox onClick={() => inputRef.current?.focus()}>
{selectedItems?.map((item) => (
<SelectedLabel>{item.label}</SelectedLabel>
))}
<AdaptiveInput ref={inputRef} onChange={onFilter} />
</StyledBox>
);
});
export const SearchBox = forwardRef<IAdaptiveInputForwardProps, ISearchBoxProps>(
({ onFilter, selectedItems, handleLabelDelete }, ref) => {
const inputRef = useRef<IAdaptiveInputForwardProps>(null);
const { theme } = useTheme();

useImperativeHandle(ref, () => ({
clear() {
inputRef.current?.clear();
},
focus() {
inputRef.current?.focus();
},
}));

const handleCrossIconClick = useCallback(
(e: React.MouseEvent<HTMLOrSVGElement>, item: ISelectItem) => {
// prevent bubble to StyledBox
e.stopPropagation();
handleLabelDelete(item);
},
[handleLabelDelete]
);

return (
<StyledBox onClick={() => inputRef.current?.focus()}>
{selectedItems?.map((item) => (
<SelectedLabel>
{item.icon ? <ItemIcon alt={item.label} src={item.icon} /> : null}
<span>{item.label}</span>
<CrossIcon color={theme.card.background} onClick={(e) => handleCrossIconClick(e, item)} />
</SelectedLabel>
))}
<AdaptiveInput ref={inputRef} onChange={onFilter} />
</StyledBox>
);
}
);
10 changes: 10 additions & 0 deletions packages/uikit/src/components/MultiSelect/assets/empty-message.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 6 additions & 2 deletions packages/uikit/src/components/MultiSelect/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,16 @@ export const Default: React.FC<React.PropsWithChildren> = () => {
<Title>MultiSelect with filter:</Title>
<MultiSelect
style={{
width: "273px",
width: "328px",
}}
scrollHeight="400px"
panelStyle={{
minHeight: "382px",
}}
scrollHeight="382px"
options={chains}
defaultValue={[chains[0].value, chains[2].value]}
isFilter
panelFooterTemplate={() => <span>Don’t see expected tokens?</span>}
/>
</Column>
<Column>
Expand Down
Loading

0 comments on commit 42dfc77

Please sign in to comment.