Improvements to Autocomplete/Dropdown/Popover
Is your feature request related to a problem? Please describe. We're looking to build a "tags" component/functionality to our app. Being inspired by the design in sites like Jira and Teamwork. Unfortunately, neither the Reshaped Autocomplete or the combination of Input + Dropdown work in our case.
Autocomplete does not work because it doesn't support multiple items being selected. Input + Dropdown does not work because the state of the Dropdown, and its focused state changes, makes the Input Field and Dropdown being visible at the same time impossible.
This can be better understood if you have a look at our current rough implementation and alternative for now.
Describe the solution you'd like
The Autocomplete component to support multiple selections. I've noticed in the HTML structure that the <input> itself is in an outer div from the startSlot in the Textfield component, which makes the layout of ( [tag1] [tag2] [a lot more tags that overflow] Placeholder here... ) impossible on the same line, even after heavily tweaking the CSS. When there are a lot of tags (startSlot) in the input, and it breaks to two lines, then the input always stays on a new line if everything is done with flex-wrap.
Another feature would be Dropdown, Popover and Autocomplete in this case: In the sandbox, if you add a lot of tags, while not closing the Popover you will notice that the Popover stays at its initial position (below the input), even when the input moves lower because of the tags overflow above. It would be a great addition for this position to update and follow its Trigger
Describe alternatives you've considered The one from the Codesandbox.
Additional context An external library that is very similar to JIra's/our idea: https://github.com/yairEO/tagify
Please let me know if you need more information, as this might appear confusing at first!
Also found this library that might be useful for references
A summary for myself: Provide an example or implement a built-in way of using Autocomplete for multi-selection. Dynamic position updates will be resolved in https://github.com/formaat-design/reshaped/issues/271
Related: https://github.com/formaat-design/reshaped/issues/285
@blvdmitry Multi-selection in the Select and Autocomplete would be much appreciated 🙏
100%, planning to add support for it in 3.2
Making some progress. I will update one of the examples in the docs with this since we already have a similar one there but I'm also trying not to include any potential business logic into this. That way you can decide where you render the values, how you dismiss them, do you want to build custom modals like in this example and so on. This means there needs to be some code on the product side for this (it's not just a component call but also a bunch of states) and Reshaped provides easy way to connect it all together.
https://github.com/user-attachments/assets/d66bb2f0-76b9-4871-bae0-eb00638107d9
For example, the code for this demo looks like this:
export const multiselect = () => {
const inputRef = React.useRef<HTMLInputElement | null>(null);
const [values, setValues] = React.useState<string[]>([]);
const [query, setQuery] = React.useState("");
const [customValueQuery, setCustomValueQuery] = React.useState("");
const customValueToggle = useToggle();
const handleDismiss = (dismissedValue: string) => {
const nextValues = values.filter((value) => value !== dismissedValue);
setValues(nextValues);
inputRef.current?.focus();
};
const handleAddCustomValue = () => {
if (customValueQuery.length) {
setValues((prev) => [...prev, customValueQuery]);
}
customValueToggle.deactivate();
setCustomValueQuery("");
};
const valuesNode = !!values.length && (
<View direction="row" gap={1}>
{values.map((value) => (
<Badge
dismissAriaLabel="Dismiss value"
onDismiss={() => handleDismiss(value)}
key={value}
size="small"
>
{value}
</Badge>
))}
</View>
);
return (
<>
<Autocomplete
name="fruit"
value={query}
placeholder="Pick your food"
startSlot={valuesNode}
inputAttributes={{ ref: inputRef }}
onBackspace={() => {
if (!query.length) handleDismiss(values[values.length - 1]);
}}
onChange={(args) => setQuery(args.value)}
onItemSelect={(args) => {
setCustomValueQuery(query);
setQuery("");
if (args.value === "_custom") {
customValueToggle.activate();
return;
}
setValues((prev) => [...prev, args.value]);
}}
>
{["Pizza", "Pie", "Ice-cream"].map((v) => {
if (!v.toLowerCase().includes(query.toLowerCase())) return;
if (values.includes(v)) return;
return (
<Autocomplete.Item key={v} value={v}>
{v}
</Autocomplete.Item>
);
})}
{!!query.length && (
<Autocomplete.Item value="_custom" icon={PlusIcon}>
Add a custom value
</Autocomplete.Item>
)}
</Autocomplete>
<Modal onClose={customValueToggle.deactivate} active={customValueToggle.active}>
<View gap={4}>
<Dismissible onClose={customValueToggle.deactivate} closeAriaLabel="Close modal">
<Modal.Title>
<Text variant="body-3" weight="medium">
Add a custom value
</Text>
</Modal.Title>
</Dismissible>
<View
direction="row"
gap={3}
as="form"
attributes={{
onSubmit: (e) => {
e.preventDefault();
handleAddCustomValue();
},
}}
>
<View.Item grow>
<TextField
name="custom"
onChange={(args) => setCustomValueQuery(args.value)}
value={customValueQuery}
/>
</View.Item>
<Button type="submit">Add</Button>
</View>
</View>
</Modal>
</>
);
};
Pretty solid first implementation!
One tricky thing to consider is the inner items (tags) breaking to another line when they take the full width of the input. Basically the thing I explain in the original comment here under "Describe the solution you'd like". You might have already solved that, but I didn't see you add a lot of items in your video. Any chance there's going to be support for that?
Also, will this work well with variant="headless" so we can have a disabled state in our app, where this can look like normal tags without an output when the parent component is inactive?
Yes, wrapping is the last feature I have written down to implement as a part of this ticket. Trying to figure out the best way to do it right now
Autocomplete supports all TextField props, so you should be able to render it as headless, switch between variants or render tags outside the input. But in case you have a use case where it's not enough - I can look into that too
Wrapping seems to be working fine and automatically now, so just adding some tests
Is the field always going to be below? Can it be on the right of the last label unless it needs to wrap? https://github.com/user-attachments/assets/c8062f1c-9725-41a5-a0bf-edefe8425809
Yep, that's exactly what happens. It wraps automatically when there is not enough space based on the flex wrapping
Released it in 3.2.0-canary.3 in case you want to try it. A few things to note here, compared to the code snippet above:
- I've added a new
multilineflag to TextField and Autocomplete to manually control when you want the input to wrap or not to make sure there are no edge cases where this behavior is not expected - For updating the autocomplete dropdown position in case you're using a multiline field, you can use
instanceRefprop and itsupdatePositionmethod.
Documentation for these changes will follow in 3.2
@its-monotype Select with multiple selected options will also follow in a ticket about a custom select dropdown: https://github.com/orgs/formaat-design/projects/2/views/1?pane=issue&itemId=38169634