Skip to content

Commit

Permalink
chore(autocomplete): add updateQueryOnSelect prop and additional demos
Browse files Browse the repository at this point in the history
  • Loading branch information
mlaursen committed Oct 27, 2024
1 parent adea03e commit 21986c4
Show file tree
Hide file tree
Showing 9 changed files with 433 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use client";
import { type State, states } from "@/constants/states.js";
import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { type ReactElement } from "react";

export default function GettingTheCurrentValueExample(): ReactElement {
const defaultValue: State = states[9];

return (
<Autocomplete
label="State"
options={states}
getOptionLabel={(state) => state.name}
defaultValue={defaultValue}
listboxLabel="States"
onValueChange={(value) => {
// Do something with the value. Should generally not call `setState`

// eslint-disable-next-line no-console
console.log("value:", value);
}}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
@use "everything";

.container {
@include everything.box-set-var(gap, 0.25rem);
@include everything.chip-set-var(height, 1.25rem);
@include everything.chip-set-var(horizontal-padding, 0.5rem);

max-width: 16rem;
width: 100%;

:global(.rmd-chip) {
font-size: 0.75rem;
}
}

.button {
@include everything.interaction-set-var(hover-background-color, transparent);

justify-content: space-between;
padding: 0;
width: 100%;

@include everything.mouse-hover {
color: #4493f8;
}
}

.dialog {
// I just grabbed the color variables in dark mode and moved them here. not
// going to do both light/dark mode
@include everything.interaction-use-dark-surface;
@include everything.theme-set-var(text-primary-color, everything.$white);
@include everything.theme-set-var(background-color, #151b23);
@include everything.list-set-var(vertical-padding, 0);
@include everything.list-set-var(item-height, auto);
@include everything.list-set-var(item-vertical-padding, 0.5rem);
@include everything.list-set-var(item-horizontal-padding, 0.5rem);
@include everything.icon-set-var(size, 1rem);
@include everything.avatar-set-var(size, 1rem);

:global(.rmd-list-item) {
@include everything.divider-border-style;

font-size: 0.75rem;
height: auto;
line-height: 1;
}
}

.title {
padding: 0.5rem 0.625rem;
}

.autocomplete {
// remove the outlined border since it is moved to the input
border: 0;
padding: 0.625rem;

// remove the outlined focus behavior since it is moved to the input
&::after {
display: none;
}

input {
background: #0d1117;
border: max(1px, 0.0625rem) solid #3d444db3;
border-radius: 0.375rem;
font-size: 0.875rem;
padding: 0.3125rem 0.75rem;

&:focus {
border-color: #1f6feb;
}
}
}

// remove the selected option background color
.option {
background: transparent;
}

.editLabels {
color: #9198a1;
gap: 0.5rem;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"use client";
import { githubLabels, type GithubLabel } from "@/constants/githubLabels.js";
import { Autocomplete } from "@react-md/core/autocomplete/Autocomplete";
import { Avatar } from "@react-md/core/avatar/Avatar";
import { Box } from "@react-md/core/box/Box";
import { Button } from "@react-md/core/button/Button";
import { Chip } from "@react-md/core/chip/Chip";
import { FixedDialog } from "@react-md/core/dialog/FixedDialog";
import { DEFAULT_OPTION_UNSELECTED_ICON } from "@react-md/core/form/Option";
import { List } from "@react-md/core/list/List";
import { ListItem } from "@react-md/core/list/ListItem";
import { ListSubheader } from "@react-md/core/list/ListSubheader";
import { BELOW_INNER_RIGHT_ANCHOR } from "@react-md/core/positioning/constants";
import { caseInsensitiveSearch } from "@react-md/core/searching/caseInsensitive";
import { contrastColor } from "@react-md/core/theme/utils";
import { Typography } from "@react-md/core/typography/Typography";
import { useToggle } from "@react-md/core/useToggle";
import CloseIcon from "@react-md/material-icons/CloseIcon";
import EditIcon from "@react-md/material-icons/EditIcon";
import SettingsIcon from "@react-md/material-icons/SettingsIcon";
import {
useId,
useRef,
useState,
type KeyboardEvent,
type ReactElement,
} from "react";
import styles from "./GithubLabelPickerExample.module.scss";

const noop = (): void => {
// do nothing
};

export default function GithubLabelPickerExample(): ReactElement {
const {
toggled: visible,
enable: show,
disable: onRequestClose,
} = useToggle();
const fixedTo = useRef<HTMLButtonElement>(null);
const titleId = useId();
const [labels, setLabels] = useState<readonly GithubLabel[]>([]);
const nextValue = useRef(labels);

// this is only required since the `setVisible` behavior is set to a no-op
// function to enforce the listbox is always visible
const handleKeyDown = (event: KeyboardEvent): void => {
if (event.key === "Escape") {
onRequestClose();
}
};

return (
<div className={styles.container}>
<Button
ref={fixedTo}
onClick={show}
className={styles.button}
disableRipple
>
Labels <SettingsIcon />
</Button>
<Box disablePadding>
{!labels.length && <Typography margin="none">None yet</Typography>}
{labels.map(({ name, color }) => (
<Chip
key={name}
style={{
background: color,
color: contrastColor(color),
}}
disableRipple
>
{name}
</Chip>
))}
</Box>
<FixedDialog
aria-labelledby={titleId}
anchor={BELOW_INNER_RIGHT_ANCHOR}
visible={visible}
fixedTo={fixedTo}
onRequestClose={onRequestClose}
disableTransition
onKeyDown={handleKeyDown}
onExited={() => {
setLabels(nextValue.current);
}}
className={styles.dialog}
>
<Typography
id={titleId}
as="h5"
type="caption"
margin="none"
className={styles.title}
textColor="text-primary"
>
Apply labels to this issue
</Typography>
<Autocomplete
aria-label="Labels"
theme="outline"
autoFocus
placeholder="Filter labels"
options={[...githubLabels].sort((a, b) => {
// sort the selected labels first
let aIndex = labels.indexOf(a);
if (aIndex === -1) {
aIndex = labels.length + githubLabels.indexOf(a);
}

let bIndex = labels.indexOf(b);
if (bIndex === -1) {
bIndex = labels.length + githubLabels.indexOf(a);
}

return aIndex - bIndex;
})}
defaultValue={labels}
onValueChange={(value) => {
nextValue.current = value;
}}
className={styles.autocomplete}
listboxLabel="Labels"
getOptionLabel={(label) => label.name}
disableInlineChips
disableCloseOnSelect
disableClearButton
disableDropdownButton
updateQueryOnSelect="as-is"
onKeyDown={handleKeyDown}
filter={(options) => caseInsensitiveSearch(options)}
listboxProps={{
disablePortal: true,
disableElevation: true,
disableTransition: true,
disableSelectedIcon: false,
disableFixedPositioning: true,
onKeyDown: handleKeyDown,
}}
getOptionProps={({ option, selected }) => {
return {
height: "auto",
className: styles.option,
rightAddon: selected && <CloseIcon />,
disableRipple: true,
children: (
<Box disablePadding>
<Avatar style={{ background: option.color }} />
{option.name}
</Box>
),
};
}}
visible
setVisible={noop}
// this would really be a dynamic creatable thing to match github, but too much for this demo
noOptionsChildren={<ListSubheader>No labels</ListSubheader>}
/>
<List>
<ListItem
height="auto"
className={styles.editLabels}
leftAddon={DEFAULT_OPTION_UNSELECTED_ICON}
disableLeftAddonSpacing
disableTextChildren
>
<Box disablePadding>
<EditIcon />
<span>Edit labels</span>
</Box>
</ListItem>
</List>
</FixedDialog>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,17 @@ be selected by default, set the `defaultValue` prop instead.
```

## Getting the current value

> !Warn! It is recommended to control the `value` instead of using this option.
When the `value` does not need to be stored anywhere but can be used for other
actions, the `onValueChange` prop can be used.

```demo source="./GettingTheCurrentValueExample.tsx"
```

## Controlling the input value

If the input value needs to be controlled, provide a `query` and `setQuery` prop.
Expand Down Expand Up @@ -270,7 +281,9 @@ custom styles, change the theme, etc.
```

# Highlights
# Customization

## Highlights

The search query can be highlighted using the
[autosuggest-highlight](https://www.npmjs.com/package/autosuggest-highlight)
Expand All @@ -281,6 +294,49 @@ option.
```

## Github's Label Picker

This demo shows how the Github label picker could be created with `react-md`
with most of the same behavior. Here's a breakdown of how this was created:

- Start by creating a simple `Button` + `FixedDialog` combination to display the
`Autocomplete`
- Create a multiselect `Autocomplete` with most of the default UI disabled to
better match Github
- `disableClearButton`
- `disableDropdownButton`
- `disableInlineChips`
- `listboxProps.disableElevation`
- Update the `Autocomplete` so that the listbox renders inline with the other
content and within the dialog
- `listboxProps.disablePortal`
- `listboxProps.disableFixedPositioning`
- Ensure the listbox is always visible by enabling the `visible` prop and
providing a noop for the `setVisible` prop. Also enable the
`disableCloseOnSelect` prop to ensure items are still filtered after selecting
an new value
- Set `updateQueryOnSelect` to `"as-is"` so that selecting a new value doesn't
change the value in the `Autocomplete`
- Update the behavior so that the selected values are only updated when the
dialog closes by:
- creating a `nextValue` ref that is mutated with the `onValueChange` prop
- setting the `defaultValue` to the `labels` state which ensures the latest
values are selected each time the dialog is opened
- add an `onExited` handler to the `Dialog` that sets the state with the
`nextValue` ref
- Allow filtering to happen anywhere within the label name by setting
`filter={(options) => caseInsensitiveSearch(options)}`
- Sort the `options` prop for the `Autocomplete` so that the selected labels
appear first
- Provide a `onKeyDown` handler for the `Dialog`, `Listbox`, and `input` that
closes the dialog when the `Escape` key is pressed
- Render the selected labels in a `Box` with `Chip` components
- Update any styles to match Github

```demo source="./GithubLabelPickerExample.tsx"
```

# Typescript Typing

The `Autocomplete` option is defined as:
Expand Down
Loading

0 comments on commit 21986c4

Please sign in to comment.