Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Picker Component #1821

Merged
merged 23 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5be7ef4
Rough first pass at Picker (#plugins-292)
bmingles Feb 20, 2024
2c773f3
Support children and items (#plugins-292)
bmingles Feb 20, 2024
fb6dadd
Item tooltip support (#plugins-292)
bmingles Feb 20, 2024
0ae647d
Split out helper component (#plugins-292)
bmingles Feb 20, 2024
c3aa11a
Tooltip permutations (#plugins-292)
bmingles Feb 20, 2024
26b7920
Split out helper props types (#plugins-292)
bmingles Feb 21, 2024
a276587
textValue support (#plugins-292)
bmingles Feb 21, 2024
fcdf1e0
Removed custom Item component (#plugins-292)
bmingles Feb 21, 2024
2231c8c
Cleanup (#plugins-292)
bmingles Feb 21, 2024
be8b64c
Cleanup (#plugins-292)
bmingles Feb 21, 2024
7212b0f
PickerChildrenOrItemsProps type (#plugins-292)
bmingles Feb 21, 2024
ba961e7
Renamed utils + comments (#plugins-292)
bmingles Feb 21, 2024
6526ee1
Moved Picker to subfolder (#plugins-292)
bmingles Feb 22, 2024
9fdc88a
Picker tests (#plugins-292)
bmingles Feb 22, 2024
bea9de7
Split out PickerItemContent (#plugins-292)
bmingles Feb 22, 2024
df50476
e2e snapshots (#plugins-292)
bmingles Feb 22, 2024
8e05713
Removed items prop (#plugins-292)
bmingles Feb 22, 2024
8a5c2ae
Renamed variables (#plugins-292)
bmingles Feb 22, 2024
3b8d49f
Added boolean support to picker item types (#plugins-292)
bmingles Feb 26, 2024
a1528e0
Added `onChange` handler (#plugins-292)
bmingles Feb 26, 2024
641b160
Typo fix (#plugins-292)
bmingles Feb 26, 2024
9316a00
Deprecated onSelectionChange (#plugins-292)
bmingles Feb 26, 2024
a8173a1
Added comments (#plugins-292)
bmingles Feb 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading