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

Add new Select component #828

Merged
merged 2 commits into from
Feb 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
55 changes: 55 additions & 0 deletions src/components/input/Select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import classnames from 'classnames';
import type { ComponentChildren, JSX } from 'preact';

import type { PresentationalProps } from '../../types';

import InputRoot from './InputRoot';

type ComponentProps = {
children?: ComponentChildren;
hasError?: boolean;
};
export type SelectProps = PresentationalProps &
ComponentProps &
JSX.HTMLAttributes<HTMLSelectElement>;

// URI-encoded source of `CaretDownIcon`
// Currently, the color (stroke) is hard-coded
const arrowImage = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' aria-hidden='true' focusable='false'%3E%3Cg fill-rule='evenodd'%3E%3Crect fill='none' stroke='none' x='0' y='0' width='16' height='16'%3E%3C/rect%3E%3Cpath fill='none' stroke='%239c9c9c' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M12 6l-4 4-4-4'%3E%3C/path%3E%3C/g%3E%3C/svg%3E")`;
Comment on lines +16 to +18
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternately, we can extend the Tailwind theme in the package's preset:

theme: {
  extend: {
    backgroundImage: {
      'caret-down': 'url("/* ... *./")',
    }
  }
}

which could then be applied with the bg-caret-down utility class.

Putting the source of the image here in the component module means we could ostensibly set stroke color or other attribute values dynamically. The up side of putting it in the tailwind preset is that, ostensibly, a consumer could override it with something completely different.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess there are pros and cons to both approaches, but since it's hard to see how the needs for this component will evolve, I would suggest keeping it like this and change it later if wee see the need.


/**
* Style a native `<select>` element.
*/
const SelectNext = function Select({
children,
classes,
hasError,
type = 'text',

...htmlAttributes
}: SelectProps) {
return (
<InputRoot
classes={classnames(
'appearance-none h-touch-minimum',
// position the down-arrow image centered at the right, offset from the
// right edge by 0.5rem. Arrow image width (4 units) + horizontal
// padding (3 units) = 7 units of right padding needed.
'bg-no-repeat bg-[center_right_0.5rem] pr-7',
classes
)}
element="select"
type={type}
hasError={hasError}
style={{
backgroundImage: arrowImage,
}}
Comment on lines +44 to +46
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The LMS implementation of this <select> uses a separate Icon component/element and absolutely-positions it at the right edge of a wrapping container element.

The advantage of using a background image is that no wrapping container is needed. This allows <Select> to take advantage of InputGroup styling, e.g.

{...htmlAttributes}
data-component="Select"
>
{children}
</InputRoot>
);
};

export default SelectNext;
1 change: 1 addition & 0 deletions src/components/input/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { default as Checkbox } from './Checkbox';
export { default as IconButton } from './IconButton';
export { default as Input } from './Input';
export { default as InputGroup } from './InputGroup';
export { default as Select } from './Select';
13 changes: 13 additions & 0 deletions src/components/input/test/Select-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { mount } from 'enzyme';

import { testPresentationalComponent } from '../../test/common-tests';

import Select from '../Select';

const contentFn = (Component, props = {}) => {
return mount(<Component aria-label="Test input" {...props} />);
};

describe('Select', () => {
testPresentationalComponent(Select, { createContent: contentFn });
});
1 change: 1 addition & 0 deletions src/next.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export {
IconButton,
Input,
InputGroup,
Select,
} from './components/input';
export {
Card,
Expand Down
117 changes: 117 additions & 0 deletions src/pattern-library/components/patterns/input/SelectPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import {
ArrowLeftIcon,
ArrowRightIcon,
IconButton,
InputGroup,
Select,
} from '../../../../next';
import Library from '../../Library';
import Next from '../../LibraryNext';

import type { SelectProps } from '../../../../components/input/Select';

function SelectWrapper({ children, ...selectProps }: SelectProps) {
const options = children ?? (
<>
<option value={-1}>All students</option>
<option value="a">Albert Banana</option>
<option value="b">Bernard California</option>
<option value="c">Cecelia Davenport</option>
<option value="d">Doris Evanescence</option>
</>
);
return <Select {...selectProps}>{options}</Select>;
}

export default function SelectPage() {
return (
<Library.Page
title="Select"
intro={
<p>
<code>Select</code> is a presentational component that styles native{' '}
<code>{'<select>'}</code> elements.
</p>
}
>
<Library.Section
title="Select"
intro={
<p>
<code>Select</code> styles <code>{'<select>'}</code> elements. Note
that <code>{'<option>'}</code> elements, with a few browser-specific
exceptions, cannot be styled with CSS.
</p>
}
>
<Library.Pattern title="Status">
<p>
<code>Select</code> is a new component.
</p>
</Library.Pattern>
<Library.Pattern title="Usage">
<Next.Usage componentName="Select" />

<Library.Example>
<Library.Demo title="Basic Select" withSource>
<div>
<SelectWrapper aria-label="Example input">
<option value={-1}>All students</option>
<option value="a">Albert Banana</option>
<option value="b">Bernard California</option>
<option value="c">Cecelia Davenport</option>
</SelectWrapper>
</div>
</Library.Demo>

<Library.Demo title="Setting Select width" withSource>
<div className="w-[250px]">
<SelectWrapper aria-label="Example input" />
</div>
</Library.Demo>

<Library.Demo title="Select in an InputGroup" withSource>
<div className="w-[380px]">
<InputGroup>
<IconButton
icon={ArrowLeftIcon}
title="Previous student"
variant="dark"
/>
<SelectWrapper aria-label="Example input" />
<IconButton
icon={ArrowRightIcon}
title="Next student"
variant="dark"
/>
</InputGroup>
</div>
</Library.Demo>
</Library.Example>
</Library.Pattern>

<Library.Pattern title="Props">
<Library.Example title="hasError">
<p>
Set <code>hasError</code> to indicate that there is an associated
error.
</p>
<Library.Demo withSource>
<div className="w-[350px]">
<SelectWrapper aria-label="Example input" hasError />
</div>
</Library.Demo>
</Library.Example>

<Library.Example title="disabled">
<Library.Demo withSource>
<div className="w-[350px]">
<SelectWrapper aria-label="Example input" disabled />
</div>
</Library.Demo>
</Library.Example>
</Library.Pattern>
</Library.Section>
</Library.Page>
);
}
7 changes: 7 additions & 0 deletions src/pattern-library/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import ButtonsPage from './components/patterns/input/ButtonPage';
import CheckboxPage from './components/patterns/input/CheckboxPage';
import InputPage from './components/patterns/input/InputPage';
import InputGroupPage from './components/patterns/input/InputGroupPage';
import SelectPage from './components/patterns/input/SelectPage';

import CardPage from './components/patterns/layout/CardPage';
import PanelPage from './components/patterns/layout/PanelPage';
Expand Down Expand Up @@ -182,6 +183,12 @@ const routes: PlaygroundRoute[] = [
component: InputGroupPage,
route: '/input-input-group',
},
{
title: 'Select',
group: 'input',
component: SelectPage,
route: '/input-select',
},
{
title: 'Card',
group: 'layout',
Expand Down