reshaped icon indicating copy to clipboard operation
reshaped copied to clipboard

Improvements to Autocomplete/Dropdown/Popover

Open vladoyoung opened this issue 1 year ago • 3 comments

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!

vladoyoung avatar May 13 '24 13:05 vladoyoung

Also found this library that might be useful for references

vladoyoung avatar Jul 05 '24 07:07 vladoyoung

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

blvdmitry avatar Jul 14 '24 23:07 blvdmitry

Related: https://github.com/formaat-design/reshaped/issues/285

blvdmitry avatar Aug 01 '24 16:08 blvdmitry

@blvdmitry Multi-selection in the Select and Autocomplete would be much appreciated 🙏

its-monotype avatar Aug 16 '24 13:08 its-monotype

100%, planning to add support for it in 3.2

blvdmitry avatar Aug 16 '24 14:08 blvdmitry

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>
		</>
	);
};

blvdmitry avatar Aug 18 '24 14:08 blvdmitry

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?

vladoyoung avatar Aug 19 '24 07:08 vladoyoung

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

blvdmitry avatar Aug 19 '24 07:08 blvdmitry

Wrapping seems to be working fine and automatically now, so just adding some tests

Image

blvdmitry avatar Aug 19 '24 21:08 blvdmitry

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

vladoyoung avatar Aug 20 '24 06:08 vladoyoung

Yep, that's exactly what happens. It wraps automatically when there is not enough space based on the flex wrapping

blvdmitry avatar Aug 20 '24 08:08 blvdmitry

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 multiline flag 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 instanceRef prop and its updatePosition method.

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

blvdmitry avatar Aug 20 '24 18:08 blvdmitry