Skip to content

Commit

Permalink
Expose Select and MultiSelect aliases for SelectNext
Browse files Browse the repository at this point in the history
  • Loading branch information
acelaya committed Jul 16, 2024
1 parent e2e88a3 commit cf8da0f
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 73 deletions.
101 changes: 60 additions & 41 deletions src/components/input/SelectNext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -257,51 +257,53 @@ type MultiValueProps<T> = {
onChange: (newValue: T[]) => void;
};

export type SelectProps<T> = CompositeProps &
(SingleValueProps<T> | MultiValueProps<T>) & {
buttonContent?: ComponentChildren;
disabled?: boolean;
type BaseSelectProps = CompositeProps & {
buttonContent?: ComponentChildren;
disabled?: boolean;

/**
* Whether this select should allow multi-selection or not.
* When this is true, the listbox is kept open when an option is selected
* and the value must be an array.
* Defaults to false.
*/
multiple?: boolean;
/**
* `id` attribute for the toggle button. This is useful to associate a label
* with the control.
*/
buttonId?: string;

/**
* `id` attribute for the toggle button. This is useful to associate a label
* with the control.
*/
buttonId?: string;
/** Additional classes to pass to container */
containerClasses?: string | string[];
/** Additional classes to pass to toggle button */
buttonClasses?: string | string[];
/** Additional classes to pass to listbox */
listboxClasses?: string | string[];

/** Additional classes to pass to container */
containerClasses?: string | string[];
/** Additional classes to pass to toggle button */
buttonClasses?: string | string[];
/** Additional classes to pass to listbox */
listboxClasses?: string | string[];
/**
* Align the listbox to the right.
* Useful when the listbox is bigger than the toggle button and this component
* is rendered next to the right side of the page/container.
* Defaults to false.
*/
right?: boolean;

/**
* Align the listbox to the right.
* Useful when the listbox is bigger than the toggle button and this component
* is rendered next to the right side of the page/container.
* Defaults to false.
*/
right?: boolean;
'aria-label'?: string;
'aria-labelledby'?: string;

/**
* Used to determine if the listbox should use the popover API.
* Defaults to true, as long as the browser supports it.
*/
listboxAsPopover?: boolean;

'aria-label'?: string;
'aria-labelledby'?: string;
/** A callback passed to the listbox onScroll */
onListboxScroll?: JSX.HTMLAttributes<HTMLUListElement>['onScroll'];
};

export type SelectNextProps<T> = BaseSelectProps &
(SingleValueProps<T> | MultiValueProps<T>) & {
/**
* Used to determine if the listbox should use the popover API.
* Defaults to true, as long as the browser supports it.
* Whether this select should allow multi-selection or not.
* When this is true, the listbox is kept open when an option is selected
* and the value must be an array.
* Defaults to false.
*/
listboxAsPopover?: boolean;

/** A callback passed to the listbox onScroll */
onListboxScroll?: JSX.HTMLAttributes<HTMLUListElement>['onScroll'];
multiple?: boolean;
};

function SelectMain<T>({
Expand All @@ -322,7 +324,7 @@ function SelectMain<T>({
'aria-labelledby': ariaLabelledBy,
/* eslint-disable-next-line no-prototype-builtins */
listboxAsPopover = HTMLElement.prototype.hasOwnProperty('popover'),
}: SelectProps<T>) {
}: SelectNextProps<T>) {
if (multiple && !Array.isArray(value)) {
throw new Error('When `multiple` is true, the value must be an array');
}
Expand Down Expand Up @@ -474,8 +476,25 @@ function SelectMain<T>({
);
}

SelectMain.displayName = 'SelectNext';
export const SelectNext = Object.assign(SelectMain, {
Option: SelectOption,
displayName: 'SelectNext',
});

export type SelectProps<T> = BaseSelectProps & SingleValueProps<T>;

export const Select = Object.assign(
function <T>(props: SelectProps<T>) {
return <SelectNext<T> {...props} />;
},
{ Option: SelectOption, displayName: 'Select' },
);

const SelectNext = Object.assign(SelectMain, { Option: SelectOption });
export type MultiSelectProps<T> = BaseSelectProps & MultiValueProps<T>;

export default SelectNext;
export const MultiSelect = Object.assign(
function <T>(props: MultiSelectProps<T>) {
return <SelectNext<T> {...props} multiple />;
},
{ Option: SelectOption, displayName: 'MultiSelect' },
);
8 changes: 6 additions & 2 deletions src/components/input/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export { default as IconButton } from './IconButton';
export { default as Input } from './Input';
export { default as InputGroup } from './InputGroup';
export { default as OptionButton } from './OptionButton';
export { default as SelectNext } from './SelectNext';
export { SelectNext, Select, MultiSelect } from './SelectNext';
export { default as Textarea } from './Textarea';

export type { ButtonProps } from './Button';
Expand All @@ -15,5 +15,9 @@ export type { IconButtonProps } from './IconButton';
export type { InputProps } from './Input';
export type { InputGroupProps } from './InputGroup';
export type { OptionButtonProps } from './OptionButton';
export type { SelectProps as SelectNextProps } from './SelectNext';
export type {
MultiSelectProps,
SelectNextProps,
SelectProps,
} from './SelectNext';
export type { TextareaProps } from './Textarea';
88 changes: 60 additions & 28 deletions src/components/input/test/SelectNext-test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { checkAccessibility, waitFor } from '@hypothesis/frontend-testing';
import { mount } from 'enzyme';

import SelectNext from '../SelectNext';
import { MultiSelect, Select, SelectNext } from '../SelectNext';

describe('SelectNext', () => {
let wrappers;
Expand All @@ -21,20 +21,26 @@ describe('SelectNext', () => {
* Whether to renders SelectNext.Option children with callback notation.
* Used primarily to test and cover both branches.
* Defaults to true.
* @param {MultiSelect | Select | SelectNext} [options.Component] -
* The actual "select" component to use. Defaults to `SelectNext`.
*/
const createComponent = (props = {}, options = {}) => {
const { paddingTop = 0, optionsChildrenAsCallback = true } = options;
const {
paddingTop = 0,
optionsChildrenAsCallback = true,
Component = SelectNext,
} = options;
const container = document.createElement('div');
container.style.paddingTop = `${paddingTop}px`;
document.body.append(container);

const wrapper = mount(
<SelectNext value={undefined} onChange={sinon.stub()} {...props}>
<SelectNext.Option value={undefined}>
<Component value={undefined} onChange={sinon.stub()} {...props}>
<Component.Option value={undefined}>
<span data-testid="reset-option">Reset</span>
</SelectNext.Option>
</Component.Option>
{items.map(item => (
<SelectNext.Option
<Component.Option
value={item}
disabled={item.id === '4'}
key={item.id}
Expand All @@ -50,9 +56,9 @@ describe('SelectNext', () => {
</span>
)
)}
</SelectNext.Option>
</Component.Option>
))}
</SelectNext>,
</Component>,
{ attachTo: container },
);

Expand Down Expand Up @@ -93,18 +99,29 @@ describe('SelectNext', () => {
const clickOption = (wrapper, id) =>
wrapper.find(`[data-testid="option-${id}"]`).simulate('click');

it('changes selected value when an option is clicked', () => {
const onChange = sinon.stub();
const wrapper = createComponent({ onChange });
[
{
title: 'changes selected value when an option is clicked',
},
{
title:
'changes selected value when an option is clicked, via Select alias',
options: { Component: Select },
},
].forEach(({ title, options = {} }) => {
it(title, () => {
const onChange = sinon.stub();
const wrapper = createComponent({ onChange }, options);

clickOption(wrapper, 3);
assert.calledWith(onChange.lastCall, items[2]);
clickOption(wrapper, 3);
assert.calledWith(onChange.lastCall, items[2]);

clickOption(wrapper, 5);
assert.calledWith(onChange.lastCall, items[4]);
clickOption(wrapper, 5);
assert.calledWith(onChange.lastCall, items[4]);

clickOption(wrapper, 1);
assert.calledWith(onChange.lastCall, items[0]);
clickOption(wrapper, 1);
assert.calledWith(onChange.lastCall, items[0]);
});
});

it('does not change selected value when a disabled option is clicked', () => {
Expand Down Expand Up @@ -409,19 +426,34 @@ describe('SelectNext', () => {
assert.isFalse(isListboxClosed(wrapper));
});

it('allows multiple items to be selected when the value is an array', () => {
const onChange = sinon.stub();
const wrapper = createComponent({
multiple: true,
value: [items[0], items[2]],
onChange,
});
[
{
title:
'allows multiple items to be selected when the value is an array',
extraProps: { multiple: true },
},
{
title: 'allows same behavior via MultiSelect alias',
options: { Component: MultiSelect },
},
].forEach(({ title, extraProps = {}, options = {} }) => {
it(title, () => {
const onChange = sinon.stub();
const wrapper = createComponent(
{
value: [items[0], items[2]],
onChange,
...extraProps,
},
options,
);

toggleListbox(wrapper);
clickOption(wrapper, 2);
toggleListbox(wrapper);
clickOption(wrapper, 2);

// When a not-yet-selected item is clicked, it will be selected
assert.calledWith(onChange, [items[0], items[2], items[1]]);
// When a not-yet-selected item is clicked, it will be selected
assert.calledWith(onChange, [items[0], items[2], items[1]]);
});
});

it('allows deselecting already selected options', () => {
Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ export {
IconButton,
Input,
InputGroup,
MultiSelect,
OptionButton,
Select,
SelectNext,
Textarea,
} from './components/input';
Expand Down Expand Up @@ -115,7 +117,9 @@ export type {
IconButtonProps,
InputProps,
InputGroupProps,
MultiSelectProps,
OptionButtonProps,
SelectProps,
SelectNextProps,
TextareaProps,
} from './components/input';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useId, useState } from 'preact/hooks';

import { Link } from '../../../..';
import type { SelectNextProps } from '../../../../components/input';
import SelectNext from '../../../../components/input/SelectNext';
import { SelectNext } from '../../../../components/input/SelectNext';
import SelectNextInInputGroup from '../../../examples/select-next-in-input-group';
import SelectNextWithManyOptions from '../../../examples/select-next-with-custom-options';
import Library from '../../Library';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useCallback, useId, useMemo, useState } from 'preact/hooks';

import { ArrowLeftIcon, ArrowRightIcon } from '../../components/icons';
import { IconButton, InputGroup } from '../../components/input';
import SelectNext from '../../components/input/SelectNext';
import { SelectNext } from '../../components/input/SelectNext';

const students = [
{ id: '1', name: 'All students' },
Expand Down

0 comments on commit cf8da0f

Please sign in to comment.