Skip to content

Commit

Permalink
fix(Select): expose render prop and default value param.s (#1781)
Browse files Browse the repository at this point in the history
- add in story docs for story example
- clean up example using unstyled and styled render props
- rename internal methods to better define code behavior and use
- remove custom styling and use FPO block instead
- add snapshots
- update existing snapshots
  • Loading branch information
booc0mtaco authored Oct 12, 2023
1 parent 76ddbc6 commit f21e2b6
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 85 deletions.
15 changes: 0 additions & 15 deletions src/components/Select/Select.stories.module.css

This file was deleted.

70 changes: 59 additions & 11 deletions src/components/Select/Select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,10 @@ import React from 'react';
import type { OptionsAlignType, VariantType } from './Select';
import { Select } from './Select';
import Icon from '../Icon';
import styles from './Select.stories.module.css';

export default {
title: 'Components/Select',
component: Select,
subcomponents: {
'Select.Button': Select.Button,
'Select.Label': Select.Label,
'Select.Options': Select.Options,
'Select.Option': Select.Option,
},
parameters: {
badges: ['1.2'],
layout: 'centered',
Expand Down Expand Up @@ -113,10 +106,7 @@ function InteractiveExampleUsingFunctionChildren() {
as={React.Fragment}
>
{() => (
<button
aria-expanded={open}
className={styles['function-children__button']}
>
<button aria-expanded={open} className="fpo">
{selectedOption?.label || 'Select'}
<Icon
className="ml-4"
Expand Down Expand Up @@ -447,3 +437,61 @@ export const WithSelectedOption: StoryObj<typeof Select> = {
/>
),
};

/**
* You can implement a `Select.Button` with a render prop. This exposes several useful values to
* control the appearance of the rendered button. The render prop case is "Headless", in that it has
* no styling by default.
*/
export const UncontrolledHeadless: StoryObj = {
render: () => (
<Select
aria-label="some label"
data-testid="dropdown"
defaultValue={exampleOptions[0]}
name="select"
>
<Select.Button>
{({ value, open, disabled }) => (
<button className="fpo">{value.label} </button>
)}
</Select.Button>
<Select.Options>
{exampleOptions.map((option) => (
<Select.Option key={option.key} value={option}>
{option.label}
</Select.Option>
))}
</Select.Options>
</Select>
),
};

/**
* You can use `Select.ButtonWrapper` to borrow the existing style used for controlled `Select` components.
*/
export const StyledUncontrolled: StoryObj = {
render: () => (
<Select
aria-label="some label"
data-testid="dropdown"
defaultValue={exampleOptions[0]}
name="select"
>
<Select.Button>
{({ value, open, disabled }) => (
<Select.ButtonWrapper isOpen={open}>
{value.label}
</Select.ButtonWrapper>
)}
</Select.Button>
<Select.Options>
{exampleOptions.map((option) => (
<Select.Option key={option.key} value={option}>
{option.label}
</Select.Option>
))}
</Select.Options>
</Select>
),
};
132 changes: 74 additions & 58 deletions src/components/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ type PropsWithRenderProp<RenderPropArg> = {
as?: ElementType;
};

let showNameWarning = true;

type SelectProps = ExtractProps<typeof Listbox> &
PopoverOptions & {
/**
Expand Down Expand Up @@ -67,6 +65,51 @@ type SelectProps = ExtractProps<typeof Listbox> &
variant?: VariantType;
};

type SelectOption = {
label: string;
[k: string]: string | number | boolean;
};

type SelectOptionProps = {
value: SelectOption;
disabled?: boolean;
className?: string;
children?: ReactNode | RenderProp<OptionRenderProps>;
};

type OptionRenderProps = {
active: boolean;
disabled: boolean;
selected: boolean;
};

type SelectButtonProps = {
/**
* Optional className for additional styling.
*/
className?: string;
/**
* Text placed inside the button to describe the field.
*/
children?: ReactNode;
/**
* Icon override for component. Default is 'expand-more'
*/
icon?: Extract<IconName, 'expand-more'>;
/**
* Indicates state of the select, used to style the button.
*/
isOpen?: boolean;
};

type SelectContextType = PopoverContext & {
compact?: boolean;
optionsAlign?: OptionsAlignType;
optionsClassName?: string;
};

let showNameWarning = true;

function childrenHaveLabelComponent(children?: ReactNode): boolean {
const childrenArray = React.Children.toArray(children);
return childrenArray.some((child) => {
Expand All @@ -86,19 +129,15 @@ function childrenHaveLabelComponent(children?: ReactNode): boolean {
});
}

type SelectContextType = PopoverContext & {
compact?: boolean;
optionsAlign?: OptionsAlignType;
optionsClassName?: string;
};

const SelectContext = React.createContext<SelectContextType>({});

/**
* `import {Select} from "@chanzuckerberg/eds";`
*
* A popover that reveals or hides a list of options from which to select
*
* Supports controlled and uncontrolled behavior, using a render prop in the latter case.
*
*/
export function Select(props: SelectProps) {
const {
Expand All @@ -112,8 +151,6 @@ export function Select(props: SelectProps) {
optionsClassName,
placement = 'bottom-start',
strategy,
// Defaulting to null is required to explicitly state that this component is controlled, and prevents warning from Headless
value = null,
variant,
...other
} = props;
Expand Down Expand Up @@ -163,7 +200,6 @@ export function Select(props: SelectProps) {
// passed directly to this component have a corresponding DOM element to receive them.
// Otherwise we get an error.
as: 'div' as const,
value,
...other,
};

Expand Down Expand Up @@ -212,8 +248,15 @@ const SelectLabel = (props: { className?: string; children: ReactNode }) => {
);
};

const SelectTrigger = function (
props: PropsWithRenderProp<{ disabled: boolean; open: boolean }>,
/**
* The trigger for the select component, which is usually a form of `Button` or some targetable/clickable component
*/
const SelectButton = function (
props: PropsWithRenderProp<{
disabled: boolean;
open: boolean;
value: SelectOption;
}>,
) {
const { children, className, ...other } = props;
const { compact, setReferenceElement } = useContext(SelectContext);
Expand All @@ -222,6 +265,7 @@ const SelectTrigger = function (
className,
compact && styles['select-button--compact'],
);

return (
<Listbox.Button
// Render as a fragment instead of the default element. We're rendering our own element in
Expand All @@ -231,13 +275,18 @@ const SelectTrigger = function (
ref={setReferenceElement}
{...other}
>
{typeof children === 'function'
? children
: ({ open }) => (
<SelectButton className={componentClassName} isOpen={open}>
{children}
</SelectButton>
)}
{(renderProps) => {
return typeof children === 'function' ? (
children(renderProps)
) : (
<SelectButtonWrapper
className={componentClassName}
isOpen={renderProps.open}
>
{children}
</SelectButtonWrapper>
);
}}
</Listbox.Button>
);
};
Expand Down Expand Up @@ -272,27 +321,12 @@ const SelectOptions = function (props: PropsWithRenderProp<{ open: boolean }>) {
return null;
};

type SelectOptionProps = {
value: any;
disabled?: boolean;
className?: string;
children?:
| ReactNode
| RenderProp<{ active: boolean; disabled: boolean; selected: boolean }>;
};

/**
* Represents one of the available options for selection
*/
const SelectOption = function (props: SelectOptionProps) {
const { children, className, ...other } = props;

type RenderProps = {
active: boolean;
disabled: boolean;
selected: boolean;
};

return (
<Listbox.Option
// Render as a fragment instead of the default <li>. We're rendering our own <li> in the
Expand All @@ -303,7 +337,7 @@ const SelectOption = function (props: SelectOptionProps) {
>
{typeof children === 'function'
? children
: ({ active, disabled, selected }: RenderProps) => {
: ({ active, disabled, selected }: OptionRenderProps) => {
return (
<PopoverListItem
active={active}
Expand All @@ -321,29 +355,10 @@ const SelectOption = function (props: SelectOptionProps) {
);
};

type SelectButtonProps = {
/**
* Optional className for additional styling.
*/
className?: string;
/**
* Text placed inside the button to describe the field.
*/
children?: ReactNode;
/**
* Icon override for component. Default is 'expand-more'
*/
icon?: Extract<IconName, 'expand-more'>;
/**
* Indicates state of the select, used to style the button.
*/
isOpen?: boolean;
};

/**
* The component functioning as a trigger, which also shows the current selection
* The component functioning as a styling for the trigger, selection arrow, and space to show the current value
*/
export const SelectButton = React.forwardRef<
export const SelectButtonWrapper = React.forwardRef<
HTMLButtonElement,
SelectButtonProps
>(({ children, className, icon = 'expand-more', isOpen, ...other }, ref) => {
Expand All @@ -367,7 +382,8 @@ export const SelectButton = React.forwardRef<
);
});

Select.Button = SelectTrigger;
Select.Button = SelectButton;
Select.ButtonWrapper = SelectButtonWrapper;
Select.Label = SelectLabel;
Select.Option = SelectOption;
Select.Options = SelectOptions;
Loading

0 comments on commit f21e2b6

Please sign in to comment.