Skip to content

Commit 3260412

Browse files
authored
Merge pull request #76 from dolthub/taylor/select-tabs
components: FormSelect.Grouped
2 parents 719058e + 38e6939 commit 3260412

22 files changed

+1011
-217
lines changed

.github/workflows/ci-web.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
yarn ci
4040
4141
- name: Publish to Chromatic
42-
if: steps.filter.outputs.components == 'true'
42+
if: steps.filter.outputs.components == 'true' && github.event.pull_request.draft == false
4343
uses: chromaui/action@latest
4444
with:
4545
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

packages/components/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"name": "DoltHub"
55
},
66
"description": "A collection of React components for common tasks",
7-
"version": "0.1.9",
7+
"version": "0.1.10",
88
"repository": {
99
"type": "git",
1010
"url": "git+https://github.com/dolthub/react-library.git"

packages/components/src/FormSelect/Async.tsx

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import React from "react";
22
import AsyncSelect from "react-select/async";
33
import Wrapper from "./Wrapper";
4-
import { formatOptionLabel, getComponents } from "./components";
5-
import customStyles, { mobileStyles } from "./styles";
4+
import { getComponents } from "./components";
5+
import { getCustomStyles } from "./styles";
66
import { AsyncProps, Option, OptionTypeBase } from "./types";
77

88
export default function FormSelectAsync<
@@ -20,25 +20,27 @@ export default function FormSelectAsync<
2020
forMobile = false,
2121
...props
2222
}: AsyncProps<T, OptionType, IsMulti>): JSX.Element {
23-
const styles = forMobile
24-
? mobileStyles<T, OptionType, IsMulti>(light)
25-
: customStyles<T, OptionType, IsMulti>(
26-
mono,
27-
light,
28-
small,
29-
pill,
30-
transparentBorder,
31-
blue,
32-
rounded,
33-
);
23+
const styles = getCustomStyles<T, OptionType, IsMulti>(
24+
mono,
25+
light,
26+
small,
27+
pill,
28+
transparentBorder,
29+
blue,
30+
rounded,
31+
forMobile,
32+
);
3433

3534
return (
3635
<Wrapper {...props} small={small}>
3736
<AsyncSelect
3837
{...props}
3938
styles={props.customStyles ? props.customStyles(styles) : styles}
40-
components={getComponents(props.components, blue, forMobile && !light)}
41-
formatOptionLabel={formatOptionLabel}
39+
components={getComponents({
40+
...props,
41+
blue,
42+
light: forMobile && !light,
43+
})}
4244
/>
4345
</Wrapper>
4446
);
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React, { useEffect, useState } from "react";
2+
import Select from "react-select";
3+
import Wrapper from "./Wrapper";
4+
import { getComponentsForGroup } from "./components";
5+
import { getCustomStyles } from "./styles";
6+
import { GroupedProps, Option, OptionTypeBase } from "./types";
7+
import { findTabIndexForValue, moveSelectedToTopForGroup } from "./utils";
8+
9+
export default function FormSelectGrouped<
10+
T,
11+
OptionType extends OptionTypeBase<T> = Option<T>,
12+
IsMulti extends boolean = false,
13+
>({
14+
mono = false,
15+
light = false,
16+
small = false,
17+
pill = false,
18+
transparentBorder = false,
19+
blue = false,
20+
rounded = false,
21+
forMobile = false,
22+
selectedOptionFirst = false,
23+
...props
24+
}: GroupedProps<T, OptionType, IsMulti>): JSX.Element {
25+
const [selectedGroupIndex, setSelectedGroupIndex] = useState(0);
26+
27+
const styles = getCustomStyles<T, OptionType, IsMulti>(
28+
mono,
29+
light,
30+
small,
31+
pill,
32+
transparentBorder,
33+
blue,
34+
rounded,
35+
forMobile,
36+
);
37+
38+
const options =
39+
selectedOptionFirst && !props.hideSelectedOptions
40+
? moveSelectedToTopForGroup(props.value, props.options)
41+
: props.options;
42+
43+
useEffect(() => {
44+
if (props.isLoading) return;
45+
const activeTabIdx = findTabIndexForValue(options, props.value);
46+
if (activeTabIdx < 0) return;
47+
setSelectedGroupIndex(activeTabIdx);
48+
}, [props.value, props.isLoading]);
49+
50+
return (
51+
<Wrapper {...props} small={small}>
52+
<Select
53+
{...props}
54+
options={options}
55+
styles={props.customStyles ? props.customStyles(styles) : styles}
56+
components={getComponentsForGroup({
57+
...props,
58+
selectedGroupIndex,
59+
setSelectedGroupIndex,
60+
blue,
61+
light: forMobile && !light,
62+
})}
63+
/>
64+
</Wrapper>
65+
);
66+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import cx from "classnames";
2+
import React from "react";
3+
import { Option } from "../types";
4+
import css from "./index.module.css";
5+
6+
type Props<OptionType> = {
7+
data: OptionType;
8+
labelPrefix: string;
9+
};
10+
11+
export default function CustomOption<T, OptionType extends Option<T>>(
12+
props: Props<OptionType>,
13+
) {
14+
const label = `${props.labelPrefix}-${props.data.value}`;
15+
return (
16+
<div
17+
className={cx({ [css.optionWithIconPath]: !!props.data.iconPath })}
18+
aria-label={label}
19+
data-cy={label}
20+
>
21+
{props.data.iconPath && (
22+
<img src={props.data.iconPath} alt={props.data.label} />
23+
)}
24+
{props.data.icon}
25+
<span>{props.data.label}</span>
26+
{props.data.details && (
27+
<div className={css.optionDetails}>{props.data.details}</div>
28+
)}
29+
</div>
30+
);
31+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import React from "react";
2+
import { GroupBase, GroupProps } from "react-select";
3+
import { OptionTypeBase } from "../types";
4+
5+
export default function Group<
6+
T,
7+
OptionType extends OptionTypeBase<T>,
8+
IsMulti extends boolean,
9+
>({ children }: GroupProps<OptionType, IsMulti, GroupBase<OptionType>>) {
10+
return <div>{children}</div>;
11+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import cx from "classnames";
2+
import React from "react";
3+
import {
4+
GroupBase,
5+
MenuProps,
6+
OptionsOrGroups,
7+
components,
8+
} from "react-select";
9+
import Btn from "../../Btn";
10+
import { CustomGroupBase, OptionTypeBase } from "../types";
11+
import css from "./index.module.css";
12+
13+
export type GroupIndexProps = {
14+
selectedGroupIndex: number;
15+
setSelectedGroupIndex: (i: number) => void;
16+
};
17+
18+
function Tabs<T, OptionType extends OptionTypeBase<T>>(
19+
props: {
20+
options: OptionsOrGroups<OptionType, GroupBase<OptionType>>;
21+
} & GroupIndexProps,
22+
) {
23+
return (
24+
<div className={css.menuWithTabs}>
25+
{props.options.map((option, index) => (
26+
<Btn
27+
key={option.label}
28+
onClick={() => props.setSelectedGroupIndex(index)}
29+
className={cx(css.tab, {
30+
[css.activeTab]: index === props.selectedGroupIndex,
31+
})}
32+
>
33+
{option.label}
34+
</Btn>
35+
))}
36+
</div>
37+
);
38+
}
39+
40+
function GroupNoOptions<T, OptionType extends OptionTypeBase<T>>(
41+
props: {
42+
options: OptionsOrGroups<OptionType, CustomGroupBase<OptionType>>;
43+
} & { selectedGroupIndex: number },
44+
) {
45+
if (props.options.length > 0) {
46+
const activeGroup = props.options[props.selectedGroupIndex];
47+
if ("options" in activeGroup) {
48+
if (activeGroup.options.length > 0) {
49+
return null;
50+
}
51+
return (
52+
<div className={css.noOpts}>
53+
{activeGroup.noOptionsMsg ?? "No options"}
54+
</div>
55+
);
56+
}
57+
}
58+
return <div className={css.noOpts}>No options</div>;
59+
}
60+
61+
function Footer<T, OptionType extends OptionTypeBase<T>>(
62+
props: {
63+
options: OptionsOrGroups<OptionType, CustomGroupBase<OptionType>>;
64+
} & { selectedGroupIndex: number },
65+
) {
66+
if (props.options.length < props.selectedGroupIndex + 1) return null;
67+
const activeGroup = props.options[props.selectedGroupIndex];
68+
if (!("footer" in activeGroup)) return null;
69+
return <div className={css.footer}>{activeGroup.footer}</div>;
70+
}
71+
72+
export default function Menu<
73+
T,
74+
OptionType extends OptionTypeBase<T>,
75+
IsMulti extends boolean,
76+
>({
77+
children,
78+
...props
79+
}: MenuProps<OptionType, IsMulti, CustomGroupBase<OptionType>> &
80+
GroupIndexProps) {
81+
return (
82+
<components.Menu {...props}>
83+
<Tabs {...props} options={props.selectProps.options} />
84+
{children}
85+
<GroupNoOptions {...props} options={props.selectProps.options} />
86+
<Footer {...props} options={props.selectProps.options} />
87+
</components.Menu>
88+
);
89+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from "react";
2+
import { MenuListProps, components } from "react-select";
3+
import { CustomGroupBase, OptionTypeBase } from "../types";
4+
import css from "./index.module.css";
5+
6+
export default function MenuList<
7+
T,
8+
OptionType extends OptionTypeBase<T>,
9+
IsMulti extends boolean,
10+
>({
11+
children,
12+
...props
13+
}: MenuListProps<OptionType, IsMulti, CustomGroupBase<OptionType>> & {
14+
selectedGroupIndex: number;
15+
}) {
16+
const activeGroup =
17+
props.options.length - 1 >= props.selectedGroupIndex
18+
? props.options[props.selectedGroupIndex]
19+
: undefined;
20+
21+
const filteredChildren = React.Children.toArray(children).filter(group => {
22+
if (typeof group === "string" || typeof group === "number") {
23+
return false;
24+
}
25+
if (!("props" in group)) return false;
26+
return group.props.data?.label === activeGroup?.label;
27+
});
28+
29+
return (
30+
<components.MenuList {...props} className={css.menuList}>
31+
{filteredChildren}
32+
</components.MenuList>
33+
);
34+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from "react";
2+
import { NoticeProps, components } from "react-select";
3+
import { CustomGroupBase, OptionTypeBase } from "../types";
4+
5+
export default function NoOptionsMessage<
6+
T,
7+
OptionType extends OptionTypeBase<T>,
8+
IsMulti extends boolean,
9+
>(
10+
props: NoticeProps<OptionType, IsMulti, CustomGroupBase<OptionType>> & {
11+
noOptionsMsg?: string;
12+
},
13+
) {
14+
return (
15+
<components.NoOptionsMessage {...props}>
16+
{props.noOptionsMsg ?? "No options"}
17+
</components.NoOptionsMessage>
18+
);
19+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { FiCheck } from "@react-icons/all-files/fi/FiCheck";
2+
import cx from "classnames";
3+
import React from "react";
4+
import { OptionProps, components } from "react-select";
5+
import { Option, OptionTypeBase } from "../types";
6+
import CustomOption from "./CustomOption";
7+
import css from "./index.module.css";
8+
9+
export default function OptionComponent<
10+
T,
11+
OptionType extends Option<T>,
12+
IsMulti extends boolean,
13+
>(props: OptionProps<OptionType, IsMulti>): JSX.Element {
14+
return (
15+
<components.Option {...props}>
16+
<CustomOption data={props.data} labelPrefix="select-option" />
17+
</components.Option>
18+
);
19+
}
20+
21+
export function OptionForGroup<
22+
T,
23+
OptionType extends OptionTypeBase<T>,
24+
IsMulti extends boolean,
25+
>(props: OptionProps<OptionType, IsMulti>): JSX.Element {
26+
return (
27+
<components.Option {...props} className={css.option}>
28+
<FiCheck
29+
className={cx(css.check, { [css.checkInvisible]: !props.isSelected })}
30+
/>
31+
<CustomOption data={props.data} labelPrefix="select-option" />
32+
</components.Option>
33+
);
34+
}

0 commit comments

Comments
 (0)