Skip to content

Commit

Permalink
feat: Picker Component (#1821)
Browse files Browse the repository at this point in the history
  • Loading branch information
bmingles authored Feb 27, 2024
1 parent 448f0f0 commit e50f0f6
Show file tree
Hide file tree
Showing 13 changed files with 465 additions and 0 deletions.
51 changes: 51 additions & 0 deletions packages/code-studio/src/styleguide/Pickers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import { Picker } from '@deephaven/components';
import { vsPerson } from '@deephaven/icons';
import { Flex, Icon, Item, Text } from '@adobe/react-spectrum';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { sampleSectionIdAndClasses } from './utils';

function PersonIcon(): JSX.Element {
return (
<Icon>
<FontAwesomeIcon icon={vsPerson} />
</Icon>
);
}

export function Pickers(): JSX.Element {
return (
// eslint-disable-next-line react/jsx-props-no-spreading
<div {...sampleSectionIdAndClasses('pickers')}>
<h2 className="ui-title">Pickers</h2>

<Flex gap={14}>
<Picker label="Single Child" tooltip={{ placement: 'bottom-end' }}>
<Item>Aaa</Item>
</Picker>

<Picker label="Mixed Children Types" tooltip>
{/* eslint-disable react/jsx-curly-brace-presence */}
{'String 1'}
{'String 2'}
{'String 3'}
{''}
{'Some really long text that should get truncated'}
{/* eslint-enable react/jsx-curly-brace-presence */}
{444}
{999}
{true}
{false}
<Item>Item Aaa</Item>
<Item>Item Bbb</Item>
<Item textValue="Complex Ccc">
<PersonIcon />
<Text>Complex Ccc</Text>
</Item>
</Picker>
</Flex>
</div>
);
}

export default Pickers;
2 changes: 2 additions & 0 deletions packages/code-studio/src/styleguide/StyleGuide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { HIDE_FROM_E2E_TESTS_CLASS } from './utils';
import { GoldenLayout } from './GoldenLayout';
import { RandomAreaPlotAnimation } from './RandomAreaPlotAnimation';
import SpectrumComparison from './SpectrumComparison';
import Pickers from './Pickers';

const stickyProps = {
position: 'sticky',
Expand Down Expand Up @@ -111,6 +112,7 @@ function StyleGuide(): React.ReactElement {
<ContextMenus />
<DropdownMenus />
<Navigations />
<Pickers />
<Tooltips />
<Icons />
<Editors />
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export { default as SelectValueList } from './SelectValueList';
export * from './SelectValueList';
export * from './shortcuts';
export { default as SocketedButton } from './SocketedButton';
export * from './spectrum';
export * from './SpectrumUtils';
export * from './TableViewEmptyState';
export * from './TextWithTooltip';
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/spectrum/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './picker';
104 changes: 104 additions & 0 deletions packages/components/src/spectrum/picker/Picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useMemo } from 'react';
import { Item, Picker as SpectrumPicker } from '@adobe/react-spectrum';
import { Tooltip } from '../../popper';
import {
NormalizedSpectrumPickerProps,
normalizePickerItemList,
normalizeTooltipOptions,
PickerItem,
PickerItemKey,
TooltipOptions,
} from './PickerUtils';
import { PickerItemContent } from './PickerItemContent';

export type PickerProps = {
children: PickerItem | PickerItem[];
/** Can be set to true or a TooltipOptions to enable item tooltips */
tooltip?: boolean | TooltipOptions;
/** The currently selected key in the collection (controlled). */
selectedKey?: PickerItemKey | null;
/** The initial selected key in the collection (uncontrolled). */
defaultSelectedKey?: PickerItemKey;
/**
* Handler that is called when the selection change.
* Note that under the hood, this is just an alias for Spectrum's
* `onSelectionChange`. We are renaming for better consistency with other
* components.
*/
onChange?: (key: PickerItemKey) => void;
/**
* Handler that is called when the selection changes.
* @deprecated Use `onChange` instead
*/
onSelectionChange?: (key: PickerItemKey) => void;
} /*
* Support remaining SpectrumPickerProps.
* Note that `selectedKey`, `defaultSelectedKey`, and `onSelectionChange` are
* re-defined above to account for boolean types which aren't included in the
* React `Key` type, but are actually supported by the Spectrum Picker component.
*/ & Omit<
NormalizedSpectrumPickerProps,
| 'children'
| 'items'
| 'onSelectionChange'
| 'selectedKey'
| 'defaultSelectedKey'
>;

/**
* Picker component for selecting items from a list of items. Items can be
* provided via the `items` prop or as children. Each item can be a string,
* number, boolean, or a Spectrum <Item> element. The remaining props are just
* pass through props for the Spectrum Picker component.
* See https://react-spectrum.adobe.com/react-spectrum/Picker.html
*/
export function Picker({
children,
tooltip,
defaultSelectedKey,
selectedKey,
onChange,
onSelectionChange,
...spectrumPickerProps
}: PickerProps): JSX.Element {
const normalizedItems = useMemo(
() => normalizePickerItemList(children),
[children]
);

const tooltipOptions = useMemo(
() => normalizeTooltipOptions(tooltip),
[tooltip]
);

return (
<SpectrumPicker
// eslint-disable-next-line react/jsx-props-no-spreading
{...spectrumPickerProps}
items={normalizedItems}
// Type assertions are necessary for `selectedKey`, `defaultSelectedKey`,
// and `onSelectionChange` due to Spectrum types not accounting for
// `boolean` keys
selectedKey={selectedKey as NormalizedSpectrumPickerProps['selectedKey']}
defaultSelectedKey={
defaultSelectedKey as NormalizedSpectrumPickerProps['defaultSelectedKey']
}
// `onChange` is just an alias for `onSelectionChange`
onSelectionChange={
(onChange ??
onSelectionChange) as NormalizedSpectrumPickerProps['onSelectionChange']
}
>
{({ content, textValue }) => (
<Item textValue={textValue === '' ? 'Empty' : textValue}>
<PickerItemContent>{content}</PickerItemContent>
{tooltipOptions == null || content === '' ? null : (
<Tooltip options={tooltipOptions}>{content}</Tooltip>
)}
</Item>
)}
</SpectrumPicker>
);
}

export default Picker;
31 changes: 31 additions & 0 deletions packages/components/src/spectrum/picker/PickerItemContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { isValidElement, ReactNode } from 'react';
import { Text } from '@adobe/react-spectrum';
import stylesCommon from '../../SpectrumComponent.module.scss';

export interface PickerItemContentProps {
children: ReactNode;
}

/**
* Picker item content. Text content will be wrapped in a Spectrum Text
* component with ellipsis overflow handling.
*/
export function PickerItemContent({
children: content,
}: PickerItemContentProps): JSX.Element {
if (isValidElement(content)) {
return content;
}

if (content === '') {
// Prevent the item height from collapsing when the content is empty
// eslint-disable-next-line no-param-reassign
content = <>&nbsp;</>;
}

return (
<Text UNSAFE_className={stylesCommon.spectrumEllipsis}>{content}</Text>
);
}

export default PickerItemContent;
142 changes: 142 additions & 0 deletions packages/components/src/spectrum/picker/PickerUtils.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import React from 'react';
import { Item, Text } from '@adobe/react-spectrum';
import {
NormalizedPickerItem,
normalizeTooltipOptions,
normalizePickerItemList,
PickerItem,
} from './PickerUtils';
import type { PickerProps } from './Picker';

beforeEach(() => {
expect.hasAssertions();
});

/* eslint-disable react/jsx-key */
const expectedNormalizations = new Map<PickerItem, NormalizedPickerItem>([
[
999,
{
content: '999',
key: 999,
textValue: '999',
},
],
[
true,
{
content: 'true',
key: true,
textValue: 'true',
},
],
[
false,
{
content: 'false',
key: false,
textValue: 'false',
},
],
[
'',
{
content: '',
key: '',
textValue: '',
},
],
[
'String',
{
content: 'String',
key: 'String',
textValue: 'String',
},
],
[
<Item>Single string child no textValue</Item>,
{
content: 'Single string child no textValue',
key: 'Single string child no textValue',
textValue: 'Single string child no textValue',
},
],
[
<Item>
<span>No textValue</span>
</Item>,
{
content: <span>No textValue</span>,
key: '',
textValue: '',
},
],
[
<Item textValue="textValue">Single string</Item>,
{
content: 'Single string',
key: 'Single string',
textValue: 'textValue',
},
],
[
<Item key="explicit.key" textValue="textValue">
Explicit key
</Item>,
{
content: 'Explicit key',
key: 'explicit.key',
textValue: 'textValue',
},
],
[
<Item textValue="textValue">
<i>i</i>
<Text>Complex</Text>
</Item>,
{
content: [<i>i</i>, <Text>Complex</Text>],
key: 'textValue',
textValue: 'textValue',
},
],
]);
/* eslint-enable react/jsx-key */

const mixedItems = [...expectedNormalizations.keys()];

const children = {
empty: [] as PickerProps['children'],
single: mixedItems[0] as PickerProps['children'],
mixed: mixedItems as PickerProps['children'],
};

describe('normalizePickerItemList', () => {
it.each([children.empty, children.single, children.mixed])(
'should return normalized picker items: %s',
given => {
const childrenArray = Array.isArray(given) ? given : [given];

const expected = childrenArray.map(item =>
expectedNormalizations.get(item)
);

const actual = normalizePickerItemList(given);
expect(actual).toEqual(expected);
}
);
});

describe('normalizeTooltipOptions', () => {
it.each([
[undefined, null],
[null, null],
[false, null],
[true, { placement: 'top-start' }],
[{ placement: 'bottom-end' }, { placement: 'bottom-end' }],
] as const)('should return: %s', (options, expected) => {
const actual = normalizeTooltipOptions(options);
expect(actual).toEqual(expected);
});
});
Loading

0 comments on commit e50f0f6

Please sign in to comment.