Skip to content

Commit

Permalink
WB-1675: Implement Combobox component with single selection (#2221)
Browse files Browse the repository at this point in the history
## Summary:

This is the first PR associated to the Combobox implementation. It includes the
basic structure of the Combobox component with single selection support.

### Combobox Implementation Plan:

1. #2216
2. **Add `Combobox` component with Single selection support. [CURRENT]**
3. Add multiple selection support to `Combobox` component.
4. Add autocomplete support to `Combobox` component.
5. Add async/dynamic support to `Combobox` component.

Issue: https://khanacademy.atlassian.net/browse/WB-1675

## Test plan:

Verify that the new `Component` component works as expected and that the new
documentation is accurate.

/?path=/docs/packages-dropdown-combobox--docs

https://5e1bf4b385e3fb0020b7073c-mzmurnrasq.chromatic.com/?path=/docs/packages-dropdown-combobox--docs

Keyboard navigation:

- Focus on the input field to open the Listbox.
- Use the `ArrowDown / ArrowUp` keys to navigate through the options.
- Use the `Enter` key to select an option.
- Use the `Escape` key to close the Listbox.
- Verify that the selected option is displayed in the input field.


https://github.com/Khan/wonder-blocks/assets/843075/64c47c5e-81f5-4e16-a22a-ccc8fcb7abb5

Author: jandrade

Reviewers: jeresig

Required Reviewers:

Approved By: jeresig

Checks: ✅ codecov/project, ✅ Chromatic - Get results on regular PRs (ubuntu-latest, 20.x), ✅ Test (ubuntu-latest, 20.x, 2/2), ✅ Test (ubuntu-latest, 20.x, 1/2), ✅ Check build sizes (ubuntu-latest, 20.x), ✅ Lint (ubuntu-latest, 20.x), ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Chromatic - Build on regular PRs / chromatic (ubuntu-latest, 20.x), ✅ Prime node_modules cache for primary configuration (ubuntu-latest, 20.x), ⏭️  Chromatic - Skip on Release PR (changesets), ✅ gerald, ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ⏭️  dependabot

Pull Request URL: #2221
  • Loading branch information
jandrade authored Aug 27, 2024
1 parent 2d270e4 commit ea506e8
Show file tree
Hide file tree
Showing 9 changed files with 925 additions and 21 deletions.
5 changes: 5 additions & 0 deletions .changeset/gold-houses-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/wonder-blocks-dropdown": minor
---

Add Combobox component with single selection support
5 changes: 5 additions & 0 deletions .changeset/swift-peaches-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@khanacademy/wonder-blocks-icon-button": minor
---

Add onMouseDown prop
172 changes: 172 additions & 0 deletions __docs__/wonder-blocks-dropdown/combobox.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import {action} from "@storybook/addon-actions";
import {Meta, StoryObj} from "@storybook/react";
import {StyleSheet} from "aphrodite";
import * as React from "react";
import {color, spacing} from "@khanacademy/wonder-blocks-tokens";
import {Combobox, OptionItem} from "@khanacademy/wonder-blocks-dropdown";
import {PropsFor, View} from "@khanacademy/wonder-blocks-core";

import ComponentInfo from "../../.storybook/components/component-info";
import packageConfig from "../../packages/wonder-blocks-dropdown/package.json";

const items = [
<OptionItem label="Banana" value="banana" key={0} />,
<OptionItem label="Strawberry" value="strawberry" disabled key={1} />,
<OptionItem label="Pear" value="pear" key={2} />,
<OptionItem label="Orange" value="orange" key={3} />,
<OptionItem label="Watermelon" value="watermelon" key={4} />,
<OptionItem label="Apple" value="apple" key={5} />,
<OptionItem label="Grape" value="grape" key={6} />,
<OptionItem label="Lemon" value="lemon" key={7} />,
<OptionItem label="Mango" value="mango" key={8} />,
];

const styles = StyleSheet.create({
example: {
background: color.offWhite,
padding: spacing.medium_16,
width: 300,
},
wrapper: {
height: 500,
},
});

const defaultArgs = {
children: items,
disabled: false,
placeholder: "Select an item",
testId: "",
};

export default {
title: "Packages / Dropdown / Combobox",
component: Combobox,
args: defaultArgs,
decorators: [
(Story): React.ReactElement<React.ComponentProps<typeof View>> => (
<View style={styles.example}>
<Story />
</View>
),
],
parameters: {
componentSubtitle: (
<ComponentInfo
name={packageConfig.name}
version={packageConfig.version}
/>
),
},
} as Meta<typeof Combobox>;

type Story = StoryObj<typeof Combobox>;

/**
* The default Combobox with a list of items.
*/
export const Default: Story = {
args: {
children: items,
},
};

/**
* Combobox supports by default single selection. This means that only one
* element can be selected from the listbox at a time. In this example, we show
* how this is done by setting a state variable in the parent component.
*/
export const SingleSelectCombobox = {
name: "Combobox with single selection",
args: {
children: items,
value: "pear",
},
};

/**
* `Combobox` can also be used in controlled mode. In this example, the selected
* value is "pear". If another item is selected, the previously selected item is
* deselected. This is the default selection type, and it is also set by
* specifying value as a `string`.
*/
export const SingleSelection: Story = {
name: "Single selection (Controlled input)",
render: function Render(args: PropsFor<typeof Combobox>) {
const [value, setValue] = React.useState(args.value);

return (
<Combobox
{...args}
value={value}
onChange={(newValue) => {
setValue(newValue);
action("onChange")(newValue);
}}
/>
);
},
args: {
children: items,
value: "pear",
},
parameters: {
chromatic: {
// we don't need screenshots because this story only tests behavior.
disableSnapshot: true,
},
},
};

/**
* `Combobox` can work as a controlled component. This can be done by setting a
* value to the `opened` prop (`true` or `false`). In this case, the parent is
* responsible for managing the opening/closing of the listbox when using this
* prop.
*
* This means that you'll also have to update `opened` to the value triggered by
* the `onToggle` prop.
*/
export const ControlledCombobox: Story = {
name: "Controlled Combobox (opened state)",
render: function Render(args: PropsFor<typeof Combobox>) {
const [opened, setOpened] = React.useState(args.opened);
const [value, setValue] = React.useState(args.value);

return (
<Combobox
{...args}
opened={opened}
onToggle={() => {
setOpened(!opened);
action("onToggle")();
}}
onChange={(newValue) => {
setValue(newValue);
action("onChange")(newValue);
}}
value={value}
/>
);
},
args: {
children: items,
opened: true,
},
decorators: [
(Story): React.ReactElement<React.ComponentProps<typeof View>> => (
<View style={styles.wrapper}>{Story()}</View>
),
],
};

/**
* A Combobox can be disabled. When disabled, the Combobox cannot be interacted
* with.
*/
export const Disabled = {
args: {
disabled: true,
value: "pear",
},
};
Loading

0 comments on commit ea506e8

Please sign in to comment.