Skip to content

Commit

Permalink
✨ feat: added dropdown label renderer
Browse files Browse the repository at this point in the history
  • Loading branch information
e1en0r committed Oct 29, 2024
1 parent cb1a654 commit e70ac47
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 44 deletions.
65 changes: 33 additions & 32 deletions src/compositions/Dropdown/DropdownContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,38 +17,37 @@ import {
DropdownSize,
} from './types';

export type LocalDropdownContentProps = ThemeProps & {
/** This is used by DropdownWithTags so an item can be added, removed and re-added */
allowReselect?: boolean;
className?: string;
disabled?: boolean;
disabledIds?: readonly string[];
emptyNotification?: DropdownEmptyProps['children'];
filter?: string;
focused?: boolean;
hideNoContent?: boolean;
/** The empty dropdown notification should be inline when using withNotification */
inlineDropdownEmpty?: boolean;
inputVariant?: DropdownInputVariant;
isDropdownVisible?: boolean;
isEmpty?: boolean;
layout: DropdownLayout;
listColor?: DropdownListColor;
listSize?: DropdownListSize;
listVariant?: DropdownListVariant;
maxSelect?: number;
minSelect?: number;
onItemFocus: PartialInteractiveListProps['onItemFocus'];
onListBlur: React.FocusEventHandler<HTMLUListElement>;
onListFocus: React.FocusEventHandler<HTMLUListElement>;
onListKeyDown: PartialInteractiveListProps['onKeyDown'];
onSelect: PartialInteractiveListProps['onSelect'];
onSelectionChange: PartialInteractiveListProps['onSelectionChange'];
onUnselect: PartialInteractiveListProps['onUnselect'];
options?: readonly DropdownOption[];
reducer: PartialInteractiveListProps['reducer'];
size: DropdownSize;
};
export type LocalDropdownContentProps = ThemeProps &
Pick<PartialInteractiveListProps, 'maxSelect' | 'minSelect' | 'renderLabel'> & {
/** This is used by DropdownWithTags so an item can be added, removed and re-added */
allowReselect?: boolean;
className?: string;
disabled?: boolean;
disabledIds?: readonly string[];
emptyNotification?: DropdownEmptyProps['children'];
filter?: string;
focused?: boolean;
hideNoContent?: boolean;
/** The empty dropdown notification should be inline when using withNotification */
inlineDropdownEmpty?: boolean;
inputVariant?: DropdownInputVariant;
isDropdownVisible?: boolean;
isEmpty?: boolean;
layout: DropdownLayout;
listColor?: DropdownListColor;
listSize?: DropdownListSize;
listVariant?: DropdownListVariant;
onItemFocus: PartialInteractiveListProps['onItemFocus'];
onListBlur: React.FocusEventHandler<HTMLUListElement>;
onListFocus: React.FocusEventHandler<HTMLUListElement>;
onListKeyDown: PartialInteractiveListProps['onKeyDown'];
onSelect: PartialInteractiveListProps['onSelect'];
onSelectionChange: PartialInteractiveListProps['onSelectionChange'];
onUnselect: PartialInteractiveListProps['onUnselect'];
options?: readonly DropdownOption[];
reducer: PartialInteractiveListProps['reducer'];
size: DropdownSize;
};

export type DropdownContentProps = MergeElementProps<'div', LocalDropdownContentProps>;

Expand Down Expand Up @@ -89,6 +88,7 @@ export function DropdownContentBase(
onUnselect,
options,
reducer,
renderLabel,
size,
themeId,
unthemed = false,
Expand Down Expand Up @@ -196,6 +196,7 @@ export function DropdownContentBase(
parentRef={containerRef}
reducer={reducer}
ref={listRef}
renderLabel={renderLabel}
rounded={layout === 'contained' && inputVariant && ['underline', 'filled'].includes(inputVariant)}
// this MUST use auto for the scroll behavior so that it plays nicely with scroll sync
scrollBehavior="auto"
Expand Down
7 changes: 6 additions & 1 deletion src/compositions/Dropdown/PartialDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import { useTranslations } from '../../hooks/useTranslations';
import { useTriggerFocus } from '../../hooks/useTriggerFocus';
import { makeCancelable } from '../../utils/makeCancelable';
import { RenderFromPropElement } from '../../utils/renderFromProp';
import { PencilSlashIcon, SearchIcon, TimesIcon } from '../../icons';
import { ArrowDownIcon } from '../../icons/ArrowDownIcon';
import { PencilSlashIcon } from '../../icons/PencilSlashIcon';
import { SearchIcon } from '../../icons/SearchIcon';
import { SpinnerIcon } from '../../icons/SpinnerIcon';
import { TimesIcon } from '../../icons/TimesIcon';
import {
FormboxContainer,
FormboxIcon,
Expand Down Expand Up @@ -94,6 +96,7 @@ export type PartialDropdownProps = Omit<
| 'maxSelect'
| 'minSelect'
| 'onItemFocus'
| 'renderLabel'
>
> &
ThemeProps & {
Expand Down Expand Up @@ -178,6 +181,7 @@ export function PartialDropdownBase(
placeholder,
readOnly = false,
reducer,
renderLabel,
searchable = false,
size = 'large',
themeId: initThemeId,
Expand Down Expand Up @@ -731,6 +735,7 @@ export function PartialDropdownBase(
options={processedOptions}
reducer={reducer}
ref={listRef}
renderLabel={renderLabel}
size={size}
themeId={themeId}
unthemed={unthemed}
Expand Down
28 changes: 28 additions & 0 deletions src/compositions/Dropdown/stories/Dropdown.docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,34 @@ export const dropdownContainerStyle = {
</Flex>
</Canvas>

### Dropdown with label renderer

<Canvas>
<Flex full wrap direction="row" justifyContent="space-between">
<Looper
list={variants}
render={variant => (
<div style={dropdownContainerStyle}>
<Dropdown
transitional
iconBefore={<TagIcon />}
inputVariant={variant}
label="Super awesome dropdown"
layout="contained"
options={options}
placeholder="Select one"
renderLabel={(label, { focused, selected }) =>
`${label}${focused ? ' is focused' : ''}${focused && selected ? ' and ' : ''}${
selected ? ' is selected' : ''
}${focused || selected ? '!' : ''}`
}
/>
</div>
)}
/>
</Flex>
</Canvas>

### Multi-select dropdown

<Canvas>
Expand Down
35 changes: 35 additions & 0 deletions src/compositions/Dropdown/stories/DropdownWithTags.docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,41 @@ export const dropdownContainerStyle = {
</div>
</Canvas>

### Dropdown with label renderer

<Canvas>
<div style={dropdownContainerStyle}>
<DropdownWithTags
transitional
initialSelected={[options[3], options[5], options[7]]}
inputVariant="filled"
label="Super awesome dropdown"
layout="contained"
options={options}
renderLabel={(label, { focused, selected }) =>
`${label}${focused ? ' is focused' : ''}${focused && selected ? ' and ' : ''}${selected ? ' is selected' : ''}${
focused || selected ? '!' : ''
}`
}
style={{ width: 300 }}
tag={({ label }) => (
<ChipContent
avatar={{ initials: 'P', color: 'primary', imgSrc: '/images/avatar.jpg' }}
icon={
<Rhythm mr={2}>
<TimesIcon scale="xsmall" />
</Rhythm>
}
size="medium"
text={label}
/>
)}
tagProps={{ flush: true }}
tagSize="medium"
/>
</div>
</Canvas>

### Custom styles

<Canvas>
Expand Down
18 changes: 14 additions & 4 deletions src/compositions/InteractiveList/InteractiveListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,24 @@ import {
} from '../../components/InteractiveGroup/InteractiveGroupItem';
import { ListItem, ListItemProps } from '../../components/List';

type StateProps = {
export type InteractiveListItemStateProps = {
disabled?: boolean;
focused?: boolean;
selected?: boolean;
};

export type LocalInteractiveListItemProps = {
id: string;
label: React.ReactChild | React.ReactFragment | ((state: StateProps) => React.ReactChild | React.ReactFragment);
label:
| React.ReactChild
| React.ReactFragment
| ((state: InteractiveListItemStateProps) => React.ReactChild | React.ReactFragment);
mimicSelectOnFocus?: boolean;
onClick: (event: React.MouseEvent | React.TouchEvent, id: LocalInteractiveListItemProps['id']) => void;
renderLabel?: (
label: React.ReactChild | React.ReactFragment,
state: InteractiveListItemStateProps,
) => React.ReactElement;
scrollBehavior?: InteractiveGroupItemProps<HTMLLIElement>['scrollBehavior'];
};

Expand All @@ -33,12 +40,13 @@ export function InteractiveListItemBase({
mimicSelectOnFocus = false,
onClick,
onKeyDown,
renderLabel,
scrollBehavior,
selected = false,
transparent = false,
...props
}: InteractiveListItemProps): JSX.Element {
const stateProps: StateProps = {
const stateProps: InteractiveListItemStateProps = {
disabled,
focused,
selected,
Expand Down Expand Up @@ -67,7 +75,9 @@ export function InteractiveListItemBase({
>)}
{...stateProps}
>
{typeof label === 'function' ? label(stateProps) : label}
{(typeof label === 'function' && label(stateProps)) ||
(renderLabel && renderLabel(label, stateProps)) ||
label}
</ListItem>
)}
</InteractiveGroupItem>
Expand Down
10 changes: 8 additions & 2 deletions src/compositions/InteractiveList/PartialInteractiveList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
PartialInteractiveGroupProviderProps,
} from '../../components/InteractiveGroup/PartialInteractiveGroupProvider';
import { List, ListProps } from '../../components/List';
import { InteractiveListItem, InteractiveListItemProps } from './InteractiveListItem';
import { InteractiveListItem, InteractiveListItemProps, InteractiveListItemStateProps } from './InteractiveListItem';

type ExplicitProviderProps = Pick<
PartialInteractiveGroupProviderProps<string, HTMLUListElement, HTMLLIElement>,
Expand Down Expand Up @@ -41,6 +41,10 @@ export type LocalPartialInteractiveListProps = ExplicitProviderProps & {
PartialInteractiveGroupProviderProps<string, HTMLUListElement, HTMLLIElement>,
keyof ExplicitProviderProps | 'children'
>;
renderLabel?: (
label: React.ReactChild | React.ReactFragment,
state: InteractiveListItemStateProps,
) => React.ReactElement;
scrollBehavior?: InteractiveListItemProps['scrollBehavior'];
unstyled?: boolean;
};
Expand Down Expand Up @@ -72,6 +76,7 @@ export function PartialInteractiveListBase(
parentRef,
providerProps,
reducer,
renderLabel,
rounded = false,
scrollBehavior,
selectOnFocus = false,
Expand Down Expand Up @@ -156,11 +161,12 @@ export function PartialInteractiveListBase(
label={label}
mimicSelectOnFocus={mimicSelectOnFocus}
onClick={handleItemClick}
renderLabel={renderLabel}
scrollBehavior={scrollBehavior}
transparent={transparent}
unstyled={unstyled}
{...stateProps}
{...(itemProps as Omit<InteractiveListItemProps, 'id' | 'label' | 'onClick'>)}
{...(itemProps as Omit<InteractiveListItemProps, 'id' | 'label' | 'onClick' | 'renderLabel'>)}
/>
);
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -343,40 +343,67 @@ NavigationRole.args = {
providerProps: { role: 'navigation', 'aria-label': 'Primary navigation' },
};

export const RenderLabel = Template.bind({});
RenderLabel.storyName = 'Render label';
RenderLabel.args = {
...defaultArgs,
renderLabel: (label, { focused, selected }) => (
<div style={{ height: '60px' }}>
{label}
{focused ? ' is focused' : ''}
{focused && selected ? ' and ' : ''}
{selected ? ' is selected' : ''}
{focused || selected ? '!' : ''}
</div>
),
};

export const LabelFunctions = Template.bind({});
LabelFunctions.storyName = 'Label functions';
LabelFunctions.storyName = 'Individual label functions';
LabelFunctions.args = {
...defaultArgs,
items: [
{
id: 'first',
label: ({ focused, selected }) => (
<div style={{ height: '60px' }}>
First {focused ? 'is focused!' : ''} {selected ? 'is selected!' : ''}
First{focused ? ' is focused' : ''}
{focused && selected ? ' and ' : ''}
{selected ? ' is selected' : ''}
{focused || selected ? '!' : ''}
</div>
),
},
{
id: 'second',
label: ({ focused, selected }) => (
<div style={{ height: '60px' }}>
Second {focused ? 'is focused!' : ''} {selected ? 'is selected!' : ''}
Second{focused ? ' is focused' : ''}
{focused && selected ? ' and ' : ''}
{selected ? ' is selected' : ''}
{focused || selected ? '!' : ''}
</div>
),
},
{
id: 'third',
label: ({ focused, selected }) => (
<div style={{ height: '60px' }}>
Third {focused ? 'is focused!' : ''} {selected ? 'is selected!' : ''}
Third{focused ? ' is focused' : ''}
{focused && selected ? ' and ' : ''}
{selected ? ' is selected' : ''}
{focused || selected ? '!' : ''}
</div>
),
},
{
id: 'fourth',
label: ({ focused, selected }) => (
<div style={{ height: '60px' }}>
Fourth {focused ? 'is focused!' : ''} {selected ? 'is selected!' : ''}
Fourth{focused ? ' is focused' : ''}
{focused && selected ? ' and ' : ''}
{selected ? ' is selected' : ''}
{focused || selected ? '!' : ''}
</div>
),
},
Expand Down

0 comments on commit e70ac47

Please sign in to comment.