Skip to content

Commit 146d421

Browse files
authored
Feat/persistent combo box (#1178)
* 🚧 Updated select components to fit persistent combo box variant * 🩹 Changed typography in list item to match design * Changes by marius.tobiassen * ✨ Persistent combobox logic for cases * ✨ Updated storybook for PersistentComboBox * 🩹 Added mode, menu to singleselect * 🚧 minor changes for types * ♻️ Changes to stories * ♻️ Minor fixes from comments * ♻️ Moved searchBar into its own component * ♻️ Remove tabindex from persistent items * 🔖 10.5.0 * 🔥 Removed console.log * 🩹 Added more checks on search bar focus logic * ♻️ Added if guard for focus on click logic * ⏪ Revert border color on dialog
1 parent a3038f7 commit 146d421

20 files changed

Lines changed: 1242 additions & 372 deletions

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@equinor/amplify-component-lib",
3-
"version": "10.4.0",
3+
"version": "10.5.0",
44
"description": "Frontend Typescript components for the Amplify team",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

src/atoms/hooks/useSelect.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,14 @@ import { flattenOptions } from 'src/molecules/Select/Select.utils';
1515
const useSelect = <T extends SelectOptionRequired>(
1616
props: SelectComponentProps<T>
1717
) => {
18-
const { loading, disabled, sortValues, onSearchChange, onOpenCallback } =
19-
props;
18+
const {
19+
loading,
20+
disabled,
21+
sortValues,
22+
onSearchChange,
23+
onOpenCallback,
24+
mode,
25+
} = props;
2026
const [open, setOpen] = useState(false);
2127
const [search, setSearch] = useState('');
2228
const searchRef = useRef<HTMLInputElement | null>(null);
@@ -78,10 +84,14 @@ const useSelect = <T extends SelectOptionRequired>(
7884
}, [selectedValues.length]);
7985

8086
const handleOnOpen = () => {
81-
if (!open && !disabled && !loading) {
87+
if (open || disabled || loading) return;
88+
if (mode === 'persistent') {
8289
searchRef.current?.focus();
83-
setOpen(true);
90+
return;
8491
}
92+
93+
searchRef.current?.focus();
94+
setOpen(true);
8595
};
8696

8797
const handleOnClose = () => {

src/molecules/ListItem/ListItem.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export const ListItem = forwardRef<HTMLButtonElement, ListItemProps>(
117117
>
118118
{leadingContent && renderContent(leadingContent)}
119119
<section>
120-
<Typography variant="button" group="navigation">
120+
<Typography variant="menu_title" group="navigation">
121121
{label}
122122
</Typography>
123123
</section>

src/molecules/Select/AddTagItem.tsx

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { FC, KeyboardEvent } from 'react';
1+
import { FC, KeyboardEvent, useMemo } from 'react';
22

3-
import { Icon } from '@equinor/eds-core-react';
3+
import { Icon, Typography } from '@equinor/eds-core-react';
44
import { add_box } from '@equinor/eds-icons';
55

66
import { colors } from 'src/atoms/style/colors';
77
import {
88
MenuItemWrapper,
9+
PersistentListItem,
910
StyledMenuItem,
1011
} from 'src/molecules/Select/Select.styles';
1112
import {
@@ -19,6 +20,7 @@ interface AddTagItemProps {
1920
onAddItem: () => void;
2021
addItemSingularWord: string;
2122
index: number;
23+
mode?: 'persistent' | 'menu';
2224
children: string;
2325
}
2426

@@ -28,6 +30,7 @@ export const AddTagItem: FC<AddTagItemProps> = ({
2830
index,
2931
onAddItem,
3032
addItemSingularWord,
33+
mode,
3134
children,
3235
}) => {
3336
const handleOnKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
@@ -39,6 +42,35 @@ export const AddTagItem: FC<AddTagItemProps> = ({
3942
}
4043
};
4144

45+
const addItemContent = useMemo(() => {
46+
return (
47+
<>
48+
<Icon data={add_box} color={colors.interactive.primary__resting.rgba} />
49+
<span>
50+
<Typography group="navigation" variant="menu_title">
51+
Add &quot;{children}&quot; as new {addItemSingularWord}
52+
</Typography>
53+
</span>
54+
</>
55+
);
56+
}, [addItemSingularWord, children]);
57+
58+
if (mode === 'persistent') {
59+
return (
60+
<MenuItemWrapper>
61+
<PersistentListItem
62+
ref={(element: HTMLButtonElement | null) => {
63+
itemRefs.current[index] = element;
64+
}}
65+
onClick={onAddItem}
66+
onKeyDownCapture={handleOnKeyDown}
67+
>
68+
{addItemContent}
69+
</PersistentListItem>
70+
</MenuItemWrapper>
71+
);
72+
}
73+
4274
return (
4375
<MenuItemWrapper>
4476
<StyledMenuItem
@@ -49,10 +81,7 @@ export const AddTagItem: FC<AddTagItemProps> = ({
4981
onClick={onAddItem}
5082
onKeyDownCapture={handleOnKeyDown}
5183
>
52-
<Icon data={add_box} color={colors.interactive.primary__resting.rgba} />
53-
<span>
54-
Add &quot;{children}&quot; as new {addItemSingularWord}
55-
</span>
84+
{addItemContent}
5685
</StyledMenuItem>
5786
</MenuItemWrapper>
5887
);

src/molecules/Select/ComboBox/ComboBox.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@ import {
77
SelectOptionRequired,
88
} from 'src/molecules/Select/Select.types';
99

10-
export type ListComboBoxProps<T extends SelectOptionRequired> =
11-
CommonSelectProps<T> & MultiSelectCommon<T> & ListSelectProps<T>;
12-
13-
export type GroupedComboBoxProps<T extends SelectOptionRequired> =
14-
CommonSelectProps<T> & MultiSelectCommon<T> & GroupedSelectProps<T>;
10+
export type ComboBoxProps<T extends SelectOptionRequired> =
11+
CommonSelectProps<T> &
12+
MultiSelectCommon<T> &
13+
(ListSelectProps<T> | GroupedSelectProps<T>);
1514

1615
/**
1716
* @param clearable - If users should be able to clear the input, defaults to true
@@ -28,7 +27,7 @@ export type GroupedComboBoxProps<T extends SelectOptionRequired> =
2827
* @param customMenuItemComponent - Custom component to use for rendering menu item, defaults to a checkbox with label
2928
*/
3029
export function ComboBox<T extends SelectOptionRequired>(
31-
props: ListComboBoxProps<T> | GroupedComboBoxProps<T>
30+
props: ComboBoxProps<T>
3231
) {
3332
return <Select {...props} />;
3433
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { KeyboardEvent, MouseEvent, ReactNode, useMemo } from 'react';
2+
3+
import { checkbox, checkbox_outline } from '@equinor/eds-icons';
4+
5+
import { colors } from 'src/atoms';
6+
import { Icon, SelectOptionRequired, Typography } from 'src/molecules';
7+
import {
8+
PersistentListItem,
9+
StyledMenuItem,
10+
} from 'src/molecules/Select/Select.styles';
11+
import {
12+
MultiSelectMenuItemProps,
13+
SelectedState,
14+
SingleSelectMenuItemProps,
15+
} from 'src/molecules/Select/Select.types';
16+
import {
17+
getParentIcon,
18+
getParentState,
19+
} from 'src/molecules/Select/SelectMenuItem.utils';
20+
21+
interface DynamicMenuItemProps<T extends SelectOptionRequired> {
22+
menuItemProps: SingleSelectMenuItemProps<T> | MultiSelectMenuItemProps<T>;
23+
isSelected: boolean;
24+
handleOnParentKeyDown?: (event: KeyboardEvent<HTMLButtonElement>) => void;
25+
children?: ReactNode;
26+
}
27+
28+
export const DynamicMenuItem = <T extends SelectOptionRequired>({
29+
menuItemProps,
30+
isSelected,
31+
handleOnParentKeyDown,
32+
children,
33+
}: DynamicMenuItemProps<T>) => {
34+
const {
35+
index,
36+
depth = 0,
37+
item,
38+
itemRefs,
39+
onItemKeyDown,
40+
onItemSelect,
41+
CustomMenuItemComponent,
42+
mode,
43+
} = menuItemProps;
44+
45+
let checkboxIcon = isSelected ? checkbox : checkbox_outline;
46+
let selectedState: SelectedState = isSelected ? 'selected' : 'none';
47+
if (item.children && item.children.length > 0 && 'values' in menuItemProps) {
48+
checkboxIcon = getParentIcon(item, menuItemProps.values);
49+
selectedState = getParentState(item, menuItemProps.values);
50+
}
51+
52+
const handleOnItemClick = (e: MouseEvent) => {
53+
// Stop form submission
54+
e.preventDefault();
55+
onItemSelect(item);
56+
};
57+
58+
const handleOnKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {
59+
if (handleOnParentKeyDown !== undefined) {
60+
handleOnParentKeyDown(e);
61+
} else {
62+
onItemKeyDown(e);
63+
}
64+
};
65+
66+
const itemContent = useMemo(() => {
67+
if (children) return children;
68+
69+
if (CustomMenuItemComponent) {
70+
return (
71+
<CustomMenuItemComponent item={item} selectedState={selectedState} />
72+
);
73+
}
74+
75+
return (
76+
<>
77+
{'values' in menuItemProps && (
78+
<Icon
79+
color={colors.interactive.primary__resting.rgba}
80+
data={checkboxIcon}
81+
/>
82+
)}
83+
<span>
84+
<Typography group="navigation" variant="menu_title">
85+
{item.label}
86+
</Typography>
87+
</span>
88+
</>
89+
);
90+
}, [
91+
CustomMenuItemComponent,
92+
checkboxIcon,
93+
children,
94+
item,
95+
menuItemProps,
96+
selectedState,
97+
]);
98+
99+
if (mode === 'persistent') {
100+
return (
101+
<PersistentListItem
102+
ref={(element: HTMLButtonElement | null) => {
103+
itemRefs.current[index] = element;
104+
}}
105+
onKeyDownCapture={handleOnKeyDown}
106+
onClick={handleOnItemClick}
107+
>
108+
{itemContent}
109+
</PersistentListItem>
110+
);
111+
}
112+
113+
return (
114+
<StyledMenuItem
115+
$depth={depth}
116+
$selected={'value' in menuItemProps ? isSelected : undefined}
117+
$paddedLeft={
118+
menuItemProps.parentHasNestedItems && 'values' in menuItemProps
119+
? menuItemProps.parentHasNestedItems
120+
: undefined
121+
}
122+
ref={(element: HTMLButtonElement | null) => {
123+
itemRefs.current[index] = element;
124+
}}
125+
index={index}
126+
closeMenuOnClick={'value' in menuItemProps}
127+
onKeyDownCapture={handleOnKeyDown}
128+
onClick={handleOnItemClick}
129+
>
130+
{itemContent}
131+
</StyledMenuItem>
132+
);
133+
};

src/molecules/Select/GroupedSelectMenu.tsx

Lines changed: 7 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,25 @@
1-
import { useMemo } from 'react';
2-
31
import { Menu } from '@equinor/eds-core-react';
42

3+
import { useGroupedSelectItems } from 'src/molecules/Select/Select.hooks';
54
import { NoItemsFoundText } from 'src/molecules/Select/Select.styles';
65
import {
7-
CustomMenuItemComponentProps,
8-
GroupedSelectProps,
9-
MultiSelectCommon,
10-
SelectMenuProps,
6+
GroupedSelectPropsCombined,
117
SelectOptionRequired,
12-
SingleSelectCommon,
138
} from 'src/molecules/Select/Select.types';
14-
import { getCumulativeArrayFromNumberedArray } from 'src/molecules/Select/Select.utils';
159
import { SelectMenuItem } from 'src/molecules/Select/SelectMenuItem';
1610

1711
export const GroupedSelectMenu = <T extends SelectOptionRequired>(
18-
props: GroupedSelectProps<T> &
19-
SelectMenuProps<T> &
20-
CustomMenuItemComponentProps<T> &
21-
(MultiSelectCommon<T> | SingleSelectCommon<T>)
12+
props: GroupedSelectPropsCombined<T>
2213
) => {
2314
const {
2415
onItemSelect,
2516
onItemKeyDown,
2617
itemRefs,
27-
groups,
28-
search,
29-
onSearchFilter,
3018
CustomMenuItemComponent,
19+
mode,
3120
} = props;
3221

33-
const filteredGroups = useMemo(() => {
34-
if (search === '') return groups;
35-
const regexPattern = new RegExp(search.trim(), 'i');
36-
return groups
37-
.map((group) => ({
38-
title: group.title,
39-
items: group.items.filter((item) => {
40-
if (onSearchFilter !== undefined) {
41-
return onSearchFilter(search, item);
42-
}
43-
44-
return item.label.match(regexPattern);
45-
}),
46-
}))
47-
.filter((group) => group.items.length > 0);
48-
}, [groups, onSearchFilter, search]);
49-
50-
const filteredGroupSum = useMemo(() => {
51-
const groupSizeArray = filteredGroups.map((group) => group.items.length);
52-
return getCumulativeArrayFromNumberedArray(groupSizeArray);
53-
}, [filteredGroups]);
22+
const { filteredGroups, filteredGroupSum } = useGroupedSelectItems(props);
5423

5524
if (filteredGroups.length === 0) {
5625
return <NoItemsFoundText>No items found</NoItemsFoundText>;
@@ -69,6 +38,7 @@ export const GroupedSelectMenu = <T extends SelectOptionRequired>(
6938
index={index + filteredGroupSum[groupIndex]}
7039
childOffset={0}
7140
item={item}
41+
mode={mode}
7242
itemRefs={itemRefs}
7343
onItemKeyDown={onItemKeyDown}
7444
onItemSelect={onItemSelect}
@@ -91,6 +61,7 @@ export const GroupedSelectMenu = <T extends SelectOptionRequired>(
9161
index={index + filteredGroupSum[groupIndex]}
9262
childOffset={0}
9363
item={item}
64+
mode={mode}
9465
itemRefs={itemRefs}
9566
onItemKeyDown={onItemKeyDown}
9667
onItemSelect={onItemSelect}

0 commit comments

Comments
 (0)