Skip to content

Commit

Permalink
feat(Select): support horizontal labels (#1962)
Browse files Browse the repository at this point in the history
- update and add snapshots and stories for this
- also add story descriptions and updated code blocks
- update display of graphical stories
  • Loading branch information
booc0mtaco authored May 23, 2024
1 parent 66b9bed commit 675ad5f
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 170 deletions.
12 changes: 12 additions & 0 deletions src/components/Select/Select-v2.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@
z-index: 100;
}

.select--label-layout-vertical {
flex-direction: column;
}

.select--label-layout-horizontal {
display: flex;
flex-direction: row;
align-items: baseline;

gap: 0.5rem;
}

/**
* The button to trigger the display of the select field.
*/
Expand Down
142 changes: 85 additions & 57 deletions src/components/Select/Select-v2.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { expect } from '@storybook/test';
import { userEvent, within } from '@storybook/testing-library';
import React from 'react';
import { Select } from './Select-v2';
import Icon from '../Icon';

const meta: Meta<typeof Select> = {
title: 'Components/V2/Select',
Expand Down Expand Up @@ -115,6 +114,7 @@ export const Default: StoryObj = {
'data-testid': 'dropdown',
defaultValue: exampleOptions[0],
name: 'select',
className: 'w-60',
children: (
<>
<Select.Button>
Expand Down Expand Up @@ -161,6 +161,18 @@ export const Default: StoryObj = {
},
};

export const HorizontalLabel: StoryObj = {
args: {
...Default.args,
className: 'w-60',
labelLayout: 'horizontal',
label: 'Animal?',
},
parameters: {
...Default.parameters,
},
};

/**
* Instead of a render prop for `Select.Button`, you can forego the render prop for the button and use static text instead.
* This mode is also useful if you want to use a controlled component and manage state yourself.
Expand Down Expand Up @@ -374,6 +386,9 @@ export const WithFieldNote: StoryObj = {
</>
),
},
parameters: {
...Default.parameters,
},
};

/**
Expand All @@ -391,7 +406,15 @@ export const UncontrolledHeadless: StoryObj = {
<>
<Select.Button>
{({ value, open, disabled }) => (
<button className="fpo">{value.label}</button>
<button className="fpo">
{
{
Birds: '🐦🦆🦜',
Dogs: '🐶🐕🐩',
Cats: '🐈🐱🐈‍⬛',
}[value.label as string]
}
</button>
)}
</Select.Button>
<Select.Options>
Expand All @@ -411,7 +434,15 @@ export const UncontrolledHeadless: StoryObj = {
<Select onChange={...}>
<Select.Button>
{({ value, open, disabled }) => (
<button className="fpo">{value.label}</button>
<button className="fpo">
{
{
Birds: '🐦🦆🦜',
Dogs: '🐶🐕🐩',
Cats: '🐈🐱🐈‍⬛',
}[value.label as string]
}
</button>
)}
</Select.Button>
<Select.Options>
Expand Down Expand Up @@ -703,6 +734,9 @@ export const SeparateButtonAndMenuWidth: StoryObj = {
diffIncludeAntiAliasing: false,
diffThreshold: 0.72,
},
docs: {
...Default.parameters?.docs,
},
},
decorators: [(Story) => <div className="p-8">{Story()}</div>],
};
Expand All @@ -720,41 +754,68 @@ export const Disabled: StoryObj = {
axe: {
disabledRules: ['color-contrast'],
},
docs: {
...Default.parameters?.docs,
},
},
};

/**
* Select fields can be marked as required by using the `required` prop.
*/
export const Required: StoryObj = {
args: {
...Default.args,
required: true,
showHint: true,
className: 'w-96',
},
parameters: {
...Default.parameters,
},
};

/**
* Fields can be marked as optional by using `required` as false, but `showHint` as true.
*/
export const Optional: StoryObj = {
args: {
...Default.args,
required: false,
showHint: true,
className: 'w-96',
},
parameters: {
...Default.parameters,
},
};

/**
* You can supply a warning field note by specifing the status of "error".
*/
export const Error: StoryObj = {
args: {
...Required.args,
isError: true,
fieldNote: 'Some text describing error',
},
parameters: {
...Required.parameters,
},
};

/**
* You can supply a warning field note by specifing the status of "warning".
*/
export const Warning: StoryObj = {
args: {
...Optional.args,
isWarning: true,
fieldNote: 'Some text describing warning',
},
parameters: {
...Optional.parameters,
},
};

/**
Expand All @@ -766,8 +827,14 @@ export const NoVisibleLabel: StoryObj = {
label: undefined,
'aria-label': 'hidden label',
},
parameters: {
...Default.parameters,
},
};

/**
* No visible label is required. In such cases, you must use an equivalent label for accessibility, like `aria-label`.
*/
export const NoVisibleLabelButRequired: StoryObj = {
args: {
...Default.args,
Expand All @@ -776,8 +843,14 @@ export const NoVisibleLabelButRequired: StoryObj = {
required: true,
className: 'w-96',
},
parameters: {
...Default.parameters,
},
};

/**
* `Select` can be both disabled and required.
*/
export const DisabledRequired: StoryObj = {
args: {
...Default.args,
Expand All @@ -790,6 +863,9 @@ export const DisabledRequired: StoryObj = {
axe: {
disabledRules: ['color-contrast'],
},
docs: {
...Default.parameters?.docs,
},
},
};

Expand All @@ -804,6 +880,9 @@ export const OptionsRightAligned: StoryObj = {
chromatic: {
delay: 300,
},
docs: {
...Default.parameters?.docs,
},
},
args: {
...Default.args,
Expand All @@ -815,60 +894,6 @@ export const OptionsRightAligned: StoryObj = {
decorators: [(Story) => <div className="p-8">{Story()}</div>],
};

/**
* As an alternative rendering method, you can use several types of render props for fine-grained control of the button rendering, and
* the rendering of the list itself. Here, we use a render prop to control the contents of `Select`
*
* For more information on `Select` render props, review: https://headlessui.com/react/listbox#using-render-props
*/
export const UsingFunctionProps: StoryObj = {
render: () => {
const [selectedOption, setSelectedOption] =
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useState<(typeof exampleOptions)[0]>();

return (
<Select
aria-label="Favorite Animal"
as="div"
data-testid="dropdown"
name="interactive-with-children"
onChange={setSelectedOption}
value={selectedOption}
>
{({ open }) => (
<>
<Select.Button
// Because we're using a render prop to completely control the styling and icon of the
// button, we need to configure this component to render as a Fragment. Otherwise we'd
// render two, nested buttons.
as={React.Fragment}
>
{() => (
<button aria-expanded={open} className="fpo">
{selectedOption?.label || 'Select'}
<Icon
className="ml-4"
name="filter-list"
purpose="decorative"
/>
</button>
)}
</Select.Button>
<Select.Options>
{exampleOptions.map((option) => (
<Select.Option key={option.key} value={option}>
{option.label}
</Select.Option>
))}
</Select.Options>
</>
)}
</Select>
);
},
};

/**
* This shows the contents of `Select` upon render. Mostly to demonstrate it is possible, to capture a snapshot of the appearance.
*/
Expand All @@ -878,6 +903,9 @@ export const OpenByDefault: StoryObj = {
badges: ['intro-1.2', 'current-2.0'],
layout: 'centered',
chromatic: { delay: 300, disableSnapshot: true },
docs: {
...Default.parameters?.docs,
},
},
play: selectCat,
};
8 changes: 8 additions & 0 deletions src/components/Select/Select-v2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ type SelectProps = ExtractProps<typeof Listbox> &
* Visible text label for the component.
*/
label?: string;
/**
* Whether the label is adjacent to the field (horizontal) or above the field (vertical)
*
* **Default is `"vertical"`**.
*/
labelLayout?: 'vertical' | 'horizontal';
/**
* Whether it should show the field hint or not
*
Expand Down Expand Up @@ -196,6 +202,7 @@ export function Select({
isError,
isWarning,
label,
labelLayout = 'vertical',
modifiers = defaultPopoverModifiers,
name,
onFirstUpdate,
Expand Down Expand Up @@ -243,6 +250,7 @@ export function Select({
const componentClassName = clsx(
styles['select'],
fieldNote && styles['select--has-fieldNote'],
labelLayout && styles[`select--label-layout-${labelLayout}`],
className,
);
const sharedProps = {
Expand Down
Loading

0 comments on commit 675ad5f

Please sign in to comment.