Skip to content

Commit 9488e41

Browse files
authored
refactor(Autocomplete)!: use Option component to define autocomplete options (#1726)
1 parent b13bf75 commit 9488e41

File tree

13 files changed

+144
-100
lines changed

13 files changed

+144
-100
lines changed

apps/docs/src/content/03-components/form-controls/autocomplete/examples/default.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import {
2-
ContextMenu,
2+
Option,
33
Label,
44
TextField,
5-
MenuItem,
65
Autocomplete,
76
} from "@mittwald/flow-react-components";
87
import { useState } from "react";
@@ -19,9 +18,9 @@ export default () => {
1918
].map((d) => {
2019
const email = `${input.split("@")[0]}@${d}`;
2120
return (
22-
<MenuItem key={email} id={email} textValue={email}>
21+
<Option key={email} value={email} textValue={email}>
2322
{email}
24-
</MenuItem>
23+
</Option>
2524
);
2625
});
2726
};
@@ -31,7 +30,7 @@ export default () => {
3130
<TextField value={input} onChange={setInput}>
3231
<Label>Email</Label>
3332
</TextField>
34-
<ContextMenu>{generateSuggestItems()}</ContextMenu>
33+
{generateSuggestItems()}
3534
</Autocomplete>
3635
);
3736
};

apps/docs/src/content/03-components/form-controls/autocomplete/examples/filter.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import {
2-
ContextMenu,
2+
Option,
33
Label,
44
TextField,
5-
MenuItem,
65
Autocomplete,
76
} from "@mittwald/flow-react-components";
87
import { useState } from "react";
@@ -19,9 +18,9 @@ export default () => {
1918
].map((d) => {
2019
const email = `${input.split("@")[0]}@${d}`;
2120
return (
22-
<MenuItem key={email} id={email} textValue={email}>
21+
<Option key={email} value={email} textValue={email}>
2322
{email}
24-
</MenuItem>
23+
</Option>
2524
);
2625
});
2726
};
@@ -38,7 +37,7 @@ export default () => {
3837
<TextField value={input} onChange={setInput}>
3938
<Label>Email</Label>
4039
</TextField>
41-
<ContextMenu>{generateSuggestItems()}</ContextMenu>
40+
{generateSuggestItems()}
4241
</Autocomplete>
4342
);
4443
};

apps/docs/src/content/03-components/form-controls/autocomplete/examples/form.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import {
44
Autocomplete,
55
Section,
66
TextField,
7-
ContextMenu,
8-
MenuItem,
7+
Option,
98
} from "@mittwald/flow-react-components";
109
import { useForm } from "react-hook-form";
1110
import {
@@ -32,9 +31,9 @@ export default () => {
3231
].map((d) => {
3332
const email = `${currentEmailValue.split("@")[0]}@${d}`;
3433
return (
35-
<MenuItem key={email} id={email} textValue={email}>
34+
<Option key={email} value={email} textValue={email}>
3635
{email}
37-
</MenuItem>
36+
</Option>
3837
);
3938
});
4039
};
@@ -52,9 +51,7 @@ export default () => {
5251
<TextField>
5352
<Label>Test</Label>
5453
</TextField>
55-
<ContextMenu>
56-
{generateSuggestItems()}
57-
</ContextMenu>
54+
{generateSuggestItems()}
5855
</Autocomplete>
5956
</Field>
6057
<Button type="submit">Speichern</Button>

apps/docs/src/content/03-components/form-controls/autocomplete/examples/search.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import {
2-
ContextMenu,
32
Label,
4-
MenuItem,
53
Autocomplete,
64
SearchField,
5+
Option,
76
} from "@mittwald/flow-react-components";
87
import { useState } from "react";
98

@@ -19,9 +18,9 @@ export default () => {
1918
].map((d) => {
2019
const email = `${input.split("@")[0]}@${d}`;
2120
return (
22-
<MenuItem key={email} id={email} textValue={email}>
21+
<Option key={email} value={email} textValue={email}>
2322
{email}
24-
</MenuItem>
23+
</Option>
2524
);
2625
});
2726
};
@@ -31,7 +30,7 @@ export default () => {
3130
<SearchField value={input} onChange={setInput}>
3231
<Label>Email</Label>
3332
</SearchField>
34-
<ContextMenu>{generateSuggestItems()}</ContextMenu>
33+
{generateSuggestItems()}
3534
</Autocomplete>
3635
);
3736
};

apps/remote-dom-demo/src/app/remote/react-hook-form/page.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ import {
1313
TextField,
1414
PasswordCreationField,
1515
Autocomplete,
16-
ContextMenu,
17-
MenuItem,
1816
} from "@mittwald/flow-remote-react-components";
1917
import {
2018
Form,
@@ -62,14 +60,12 @@ export default function Page() {
6260
<TextField>
6361
<Label>Email</Label>
6462
</TextField>
65-
<ContextMenu>
66-
<MenuItem textValue="Foo" id="Foo">
67-
Foo
68-
</MenuItem>
69-
<MenuItem textValue="Bar" id="Bar">
70-
Bar
71-
</MenuItem>
72-
</ContextMenu>
63+
<Option textValue="Foo" value="Foo">
64+
Foo
65+
</Option>
66+
<Option textValue="Bar" value="Bar">
67+
Bar
68+
</Option>
7369
</Autocomplete>
7470
</Field>
7571
<Field name="comment">

apps/remote-dom-demo/src/app/remote/simple-form/page.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ import {
1313
CheckboxGroup,
1414
Checkbox,
1515
Button,
16-
ContextMenu,
17-
MenuItem,
1816
Autocomplete,
1917
} from "@mittwald/flow-remote-react-components";
2018
import { useState } from "react";
@@ -53,14 +51,12 @@ export default function Page() {
5351
</RadioGroup>
5452
<Autocomplete>
5553
<TextField name="text" aria-label="Text" />
56-
<ContextMenu>
57-
<MenuItem textValue="Foo" id="Foo">
58-
Foo
59-
</MenuItem>
60-
<MenuItem textValue="Bar" id="Bar">
61-
Bar
62-
</MenuItem>
63-
</ContextMenu>
54+
<Option textValue="Foo" value="Foo">
55+
Foo
56+
</Option>
57+
<Option textValue="Bar" value="Bar">
58+
Bar
59+
</Option>
6460
</Autocomplete>
6561

6662
<Select name="select" aria-label="Select">
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
.autocomplete {
2+
display: flex;
3+
}
4+
15
.empty {
26
padding: var(--size-px--xs) var(--size-px--s);
37
}

packages/components/src/components/Autocomplete/Autocomplete.tsx

Lines changed: 63 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,18 @@ import {
77
flowComponent,
88
type FlowComponentProps,
99
} from "@/lib/componentFactory/flowComponent";
10-
import { useLocalizedStringFormatter } from "react-aria";
11-
import styles from "./Autocomplete.module.scss";
12-
import locales from "./locales/*.locale.json";
13-
import { Text } from "@/components/Text";
1410
import type { SearchFieldProps } from "@/components/SearchField";
1511
import type { TextFieldProps } from "@/components/TextField";
16-
12+
import Options from "@/components/Options";
13+
import { TunnelExit } from "@mittwald/react-tunnel";
14+
import locales from "./locales/*.locale.json";
15+
import Text from "@/components/Text";
16+
import styles from "./Autocomplete.module.scss";
17+
import {
18+
UNSAFE_PortalProvider,
19+
useFocusWithin,
20+
useLocalizedStringFormatter,
21+
} from "react-aria";
1722
export interface AutocompleteProps
1823
extends PropsWithChildren,
1924
PropsWithClassName,
@@ -23,17 +28,21 @@ export interface AutocompleteProps
2328
/** @flr-generate all */
2429
export const Autocomplete = flowComponent("Autocomplete", (props) => {
2530
const { children, ...rest } = props;
26-
const stringFormatter = useLocalizedStringFormatter(locales);
2731

2832
const { contains } = Aria.useFilter({ sensitivity: "base" });
29-
33+
const stringFormatter = useLocalizedStringFormatter(locales);
34+
const container = useRef(null);
3035
const triggerRef = useRef<HTMLInputElement>(null);
3136

32-
const controller = useOverlayController("ContextMenu", {
37+
const controller = useOverlayController("Popover", {
3338
reuseControllerFromContext: false,
3439
});
3540
const menuIsOpen = controller.useIsOpen();
3641

42+
const focusWithin = useFocusWithin({
43+
onBlurWithin: controller.close,
44+
});
45+
3746
const inputProps: SearchFieldProps & TextFieldProps = {
3847
onKeyDown: (e) => {
3948
if (e.key === "Enter" && menuIsOpen) {
@@ -43,32 +52,18 @@ export const Autocomplete = flowComponent("Autocomplete", (props) => {
4352
ref: triggerRef,
4453
};
4554

55+
const renderEmptyState = () => (
56+
<Text className={styles.empty}>
57+
{stringFormatter.format("autocomplete.empty")}
58+
</Text>
59+
);
60+
4661
const propsContext: PropsContext = {
47-
ContextMenu: {
48-
placement: "bottom start",
49-
controller,
50-
isNonModal: true,
51-
renderEmptyState: () => (
52-
<Text className={styles.empty}>
53-
{stringFormatter.format("autocomplete.empty")}
54-
</Text>
55-
),
56-
onAction: (key) => {
57-
const input = triggerRef.current;
58-
if (input) {
59-
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
60-
window.HTMLInputElement.prototype,
61-
"value",
62-
)?.set;
63-
nativeInputValueSetter?.call(input, String(key));
64-
const event = new Event("input", { bubbles: true });
65-
input.dispatchEvent(event);
66-
}
67-
},
68-
triggerRef,
69-
},
7062
SearchField: inputProps,
7163
TextField: inputProps,
64+
Option: {
65+
tunnelId: "options",
66+
},
7267
};
7368

7469
const handleOnInputChange = (value: string) => {
@@ -79,19 +74,49 @@ export const Autocomplete = flowComponent("Autocomplete", (props) => {
7974
}
8075
};
8176

77+
const handleOptionAction = (key: Aria.Key) => {
78+
const inputElement = triggerRef.current;
79+
if (inputElement) {
80+
// Set value on input element and trigger change event
81+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
82+
window.HTMLInputElement.prototype,
83+
"value",
84+
)?.set;
85+
nativeInputValueSetter?.call(inputElement, String(key));
86+
const event = new Event("change", { bubbles: true });
87+
inputElement.dispatchEvent(event);
88+
}
89+
controller.close();
90+
};
91+
8292
return (
8393
<PropsContextProvider
8494
props={propsContext}
8595
mergeInParentContext
8696
dependencies={[menuIsOpen, controller]}
8797
>
88-
<Aria.Autocomplete
89-
onInputChange={handleOnInputChange}
90-
filter={contains}
91-
{...rest}
92-
>
93-
{children}
94-
</Aria.Autocomplete>
98+
<div {...focusWithin.focusWithinProps} ref={container}>
99+
<UNSAFE_PortalProvider getContainer={() => container.current}>
100+
<Aria.Autocomplete
101+
onInputChange={handleOnInputChange}
102+
filter={contains}
103+
disableAutoFocusFirst
104+
{...rest}
105+
>
106+
{children}
107+
<Options
108+
onAction={handleOptionAction}
109+
triggerRef={triggerRef}
110+
controller={controller}
111+
renderEmptyState={renderEmptyState}
112+
isNonModal
113+
placement="bottom start"
114+
>
115+
<TunnelExit id="options" />
116+
</Options>
117+
</Aria.Autocomplete>
118+
</UNSAFE_PortalProvider>
119+
</div>
95120
</PropsContextProvider>
96121
);
97122
});

packages/components/src/components/Autocomplete/stories/Default.stories.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import type { Chat } from "@/components/Chat";
44
import { Autocomplete } from "@/components/Autocomplete";
55
import { Label } from "@/components/Label";
66
import { SearchField } from "@/components/SearchField";
7-
import { MenuItem } from "@/components/MenuItem";
8-
import { ContextMenu } from "@/components/ContextMenu";
7+
import Option from "@/components/Option";
98

109
const meta: Meta<typeof Chat> = {
1110
title: "Form Controls/Autocomplete",
@@ -18,9 +17,9 @@ const meta: Meta<typeof Chat> = {
1817
return ["example.com", "test.org", "email.net", "mail.com"].map((d) => {
1918
const email = `${value.split("@")[0]}@${d}`;
2019
return (
21-
<MenuItem key={email} id={email} textValue={email}>
20+
<Option key={email} value={email} textValue={email}>
2221
{email}
23-
</MenuItem>
22+
</Option>
2423
);
2524
});
2625
};
@@ -32,13 +31,26 @@ const meta: Meta<typeof Chat> = {
3231
<SearchField onChange={setInput} value={input}>
3332
<Label>Test</Label>
3433
</SearchField>
35-
<ContextMenu>{suggestEmail(input)}</ContextMenu>
34+
{suggestEmail(input)}
3635
</Autocomplete>
3736
);
3837
},
3938
};
4039
export default meta;
4140

41+
export const FixedOptions: Story = {
42+
render: () => (
43+
<Autocomplete>
44+
<SearchField>
45+
<Label>Test</Label>
46+
</SearchField>
47+
<Option value="example.com">example.com</Option>
48+
<Option value="domain.de">domain.de</Option>
49+
<Option value="test.org">test.org</Option>
50+
</Autocomplete>
51+
),
52+
};
53+
4254
type Story = StoryObj<typeof Autocomplete>;
4355

4456
export const Default: Story = {};

0 commit comments

Comments
 (0)