diff --git a/src/components/InputList/InputList.js b/src/components/InputList/InputList.js
new file mode 100644
index 0000000..f4146a3
--- /dev/null
+++ b/src/components/InputList/InputList.js
@@ -0,0 +1,58 @@
+import classnames from 'classnames';
+import { func, oneOf, string, arrayOf, shape, oneOfType } from 'prop-types';
+import React from 'react';
+import * as olt from '@lightelligence/styles';
+import { InputListItem } from './InputListItem';
+
+/**
+ * Input list is a list of items, used as the body of a dropdown. Can be used
+ * for any custom component
+ *
+ * This component uses a semantic `ul` html tag name and forwards refs
+ *
+ * The component also passes all other `props` to the underlying `ul` element
+ */
+export const InputList = React.forwardRef(
+ ({ className, children, onChange, value, ...props }, ref) => (
+
+ {React.Children.map(children, (child) =>
+ React.cloneElement(child, {
+ active: child.props.value === value,
+ onClick: onChange,
+ }),
+ )}
+
+ ),
+);
+
+InputList.displayName = 'InputList';
+
+InputList.propTypes = {
+ /**
+ * Forward an additional className to the underlying element
+ */
+ className: string,
+ /**
+ * Content of the element should always be consisted of
+ * [InputListItem](/#/Components/InputListItem) components.
+ */
+ children: oneOfType([
+ shape({ type: oneOf([InputListItem]) }),
+ arrayOf(shape({ type: oneOf([InputListItem]) })),
+ ]),
+ /**
+ * Callback when the value of the input list was changed
+ */
+ onChange: func,
+ /**
+ * The current value of the input list
+ */
+ value: string,
+};
+
+InputList.defaultProps = {
+ className: null,
+ children: null,
+ onChange: () => {},
+ value: null,
+};
diff --git a/src/components/InputList/InputList.md b/src/components/InputList/InputList.md
new file mode 100644
index 0000000..486cb71
--- /dev/null
+++ b/src/components/InputList/InputList.md
@@ -0,0 +1,14 @@
+### Example
+
+```jsx
+import { InputList, InputListItem } from '@lightelligence/react';
+const [value, setValue] = React.useState('2');
+const onChange = (value) => {
+ setValue(value);
+};
+
+ Item 1
+ Item 2
+ Item 3
+;
+```
diff --git a/src/components/InputList/InputList.test.js b/src/components/InputList/InputList.test.js
new file mode 100644
index 0000000..c2930ec
--- /dev/null
+++ b/src/components/InputList/InputList.test.js
@@ -0,0 +1,61 @@
+import React from 'react';
+import { fireEvent, render } from '@testing-library/react';
+
+import { InputList } from './InputList';
+import { InputListItem } from './InputListItem';
+
+const renderComponent = (props) => {
+ return render();
+};
+
+describe('InputList', () => {
+ test('forwards className', () => {
+ const { getByTestId } = renderComponent({
+ className: 'myClass',
+ value: '1',
+ children: [
+ {}} value="1" />,
+ {}} value="2" />,
+ ],
+ });
+ const component = getByTestId('component');
+ expect(component.classList.contains('myClass')).toBe(true);
+ });
+ test('properly sets value', () => {
+ const { getByText } = renderComponent({
+ className: 'myClass',
+ value: '1',
+ children: [
+ {}} value="1">
+ Item Foo
+ ,
+ {}} value="2">
+ Item Bar
+ ,
+ ],
+ });
+ const itemFoo = getByText('Item Foo');
+ const itemBar = getByText('Item Bar');
+ expect(itemFoo.classList.contains('is-active')).toBe(true);
+ expect(itemBar.classList.contains('is-active')).toBe(false);
+ });
+ test('properly propagates onChange', () => {
+ const onChange = jest.fn();
+ const { getByText } = renderComponent({
+ className: 'myClass',
+ onChange,
+ value: '1',
+ children: [
+ {}} value="1">
+ Item Foo
+ ,
+ {}} value="2">
+ Item Bar
+ ,
+ ],
+ });
+ const itemBar = getByText('Item Bar');
+ fireEvent.click(itemBar);
+ expect(onChange).toHaveBeenCalledWith('2', expect.anything());
+ });
+});
diff --git a/src/components/InputList/InputListItem.js b/src/components/InputList/InputListItem.js
new file mode 100644
index 0000000..938d3a2
--- /dev/null
+++ b/src/components/InputList/InputListItem.js
@@ -0,0 +1,106 @@
+import classnames from 'classnames';
+import { bool, node, string, func, number } from 'prop-types';
+import React, { useCallback } from 'react';
+import * as olt from '@lightelligence/styles';
+
+/**
+ * List item for the [InputList](#/Components/InputList) Component
+ *
+ * This component uses a semantic `li` html tag name and forwards refs
+ *
+ * The component also passes all other `props` to the underlying `li` element
+ */
+export const InputListItem = React.forwardRef(
+ (
+ {
+ className,
+ children,
+ active,
+ onClick,
+ onKeyPress,
+ value,
+ tabIndex,
+ ...props
+ },
+ ref,
+ ) => {
+ const handleClick = useCallback((event) => onClick(value, event), [
+ value,
+ onClick,
+ ]);
+ const handleKeyPress = useCallback((event) => onKeyPress(value, event), [
+ value,
+ onKeyPress,
+ ]);
+
+ return (
+
+
+ {children}
+
+
+ );
+ },
+);
+
+InputListItem.displayName = 'InputListItem';
+
+InputListItem.propTypes = {
+ /**
+ * Forward an additional className to the underlying element
+ */
+ className: string,
+ /**
+ * Content of the element. Can be any valid React node.
+ */
+ children: node,
+ /**
+ * The value of the item when it's being selected
+ */
+ value: string.isRequired,
+ /**
+ * Tab index
+ *
+ * @hidden
+ */
+ tabIndex: number,
+ /**
+ * Is the item currently active
+ *
+ * @hidden
+ */
+ active: bool,
+ /**
+ * Specifies what happens when the item is clicked
+ *
+ * @hidden
+ */
+ onClick: func,
+ /**
+ * Specifies what happens on key press
+ *
+ * @hidden
+ */
+ onKeyPress: func,
+};
+
+InputListItem.defaultProps = {
+ className: null,
+ children: null,
+ active: false,
+ onKeyPress: null,
+ tabIndex: null,
+ onClick: () => {},
+};
diff --git a/src/components/InputList/InputListItem.md b/src/components/InputList/InputListItem.md
new file mode 100644
index 0000000..6c5e152
--- /dev/null
+++ b/src/components/InputList/InputListItem.md
@@ -0,0 +1,14 @@
+
+### Example
+
+```jsx
+import { InputList, InputListItem } from '@lightelligence/react';
+const onChange = (value) => {
+ console.log(`changed to ${value}`);
+};
+
+ Item 1
+ Item 2
+ Item 3
+;
+```
diff --git a/src/components/InputList/InputListItem.test.js b/src/components/InputList/InputListItem.test.js
new file mode 100644
index 0000000..95ac457
--- /dev/null
+++ b/src/components/InputList/InputListItem.test.js
@@ -0,0 +1,45 @@
+import React from 'react';
+import { fireEvent, render } from '@testing-library/react';
+
+import { InputListItem } from './InputListItem';
+
+const renderComponent = (props) => {
+ return render();
+};
+
+describe('InputListItem', () => {
+ test('forwards className', () => {
+ const { getByText } = renderComponent({
+ className: 'myClass',
+ onClick: () => {},
+ value: '1',
+ children: 'Component',
+ });
+
+ const component = getByText('Component');
+ expect(component.classList.contains('myClass')).toBe(true);
+ });
+ test('forwards onClick', () => {
+ const onClick = jest.fn();
+ const { getByText } = renderComponent({
+ className: 'myClass',
+ onClick,
+ value: '1',
+ children: 'Component',
+ });
+ const component = getByText('Component');
+ fireEvent.click(component);
+ expect(onClick).toHaveBeenCalledWith('1', expect.anything());
+ });
+ test('forwards active', () => {
+ const { getByText } = renderComponent({
+ className: 'myClass',
+ onClick: () => {},
+ value: '1',
+ active: true,
+ children: 'Component',
+ });
+ const component = getByText('Component');
+ expect(component.classList.contains('is-active')).toBe(true);
+ });
+});
diff --git a/src/components/InputList/index.js b/src/components/InputList/index.js
new file mode 100644
index 0000000..5979b72
--- /dev/null
+++ b/src/components/InputList/index.js
@@ -0,0 +1,2 @@
+export { InputList } from './InputList';
+export { InputListItem } from './InputListItem';
diff --git a/src/components/V2Button/V2Button.js b/src/components/V2Button/V2Button.js
index ff68d63..bfb5b68 100644
--- a/src/components/V2Button/V2Button.js
+++ b/src/components/V2Button/V2Button.js
@@ -47,6 +47,8 @@ const V2Button = React.forwardRef(
},
);
+V2Button.displayName = 'V2Button';
+
V2Button.propTypes = {
/**
* The html tag that should be rendered for this button.
diff --git a/src/components/V2Dropdown/V2Dropdown.js b/src/components/V2Dropdown/V2Dropdown.js
new file mode 100644
index 0000000..c452d65
--- /dev/null
+++ b/src/components/V2Dropdown/V2Dropdown.js
@@ -0,0 +1,123 @@
+import classnames from 'classnames';
+import { string, oneOfType, shape, oneOf, arrayOf, func } from 'prop-types';
+import React, { useCallback, useState } from 'react';
+import * as olt from '@lightelligence/styles';
+import { V2Label } from '../../controls/V2Label';
+import { InputListItem, InputList } from '../InputList';
+
+export const V2Dropdown = React.forwardRef(
+ (
+ {
+ className,
+ label,
+ children,
+ value,
+ onChange,
+ labelProps,
+ selectedContentProps,
+ inputListProps,
+ ...props
+ },
+ ref,
+ ) => {
+ const [isOpen, setOpen] = useState(false);
+
+ const onClick = useCallback(() => {
+ setOpen(!isOpen);
+ }, [isOpen, setOpen]);
+
+ const selectedChild = React.Children.toArray(children).find(
+ (child) => child.props.value === value,
+ );
+ const selectedElement = selectedChild && React.cloneElement(selectedChild);
+
+ return (
+
+
+ {selectedElement && (
+
+ {selectedElement.props.children}
+
+ )}
+
+ {children}
+
+
+
+ );
+ },
+);
+
+V2Dropdown.displayName = 'V2Dropdown';
+
+V2Dropdown.propTypes = {
+ /**
+ * The floating label
+ */
+ label: string.isRequired,
+ /**
+ * Forward an additional className to the underlying element
+ */
+ className: string,
+ /**
+ * Content of the element should always be consisted of
+ * [InputListItem](/#/Components/InputListItem) components.
+ */
+ children: oneOfType([
+ shape({ type: oneOf([InputListItem]) }),
+ arrayOf(shape({ type: oneOf([InputListItem]) })),
+ ]),
+ /**
+ * The current value of the input list
+ */
+ value: string,
+ /**
+ * Callback when the value of the input list was changed
+ */
+ onChange: func,
+ /**
+ * Additional label props
+ */
+ labelProps: shape({}),
+ /**
+ * Additional selected content props
+ */
+ selectedContentProps: shape({}),
+ /**
+ * Additional input list props
+ */
+ inputListProps: shape({}),
+};
+
+V2Dropdown.defaultProps = {
+ className: null,
+ children: null,
+ value: null,
+ labelProps: {},
+ selectedContentProps: {},
+ inputListProps: {},
+ onChange: () => {},
+};
diff --git a/src/components/V2Dropdown/V2Dropdown.md b/src/components/V2Dropdown/V2Dropdown.md
new file mode 100644
index 0000000..0902c0f
--- /dev/null
+++ b/src/components/V2Dropdown/V2Dropdown.md
@@ -0,0 +1,17 @@
+
+### Example
+
+```jsx
+import { InputListItem } from "@lightelligence/react";
+const [value, setValue] = React.useState('2');
+const onChange = (value) => {
+ setValue(value);
+};
+
+
+ Item 1
+ Item 2
+ Item 3
+
+
+```
diff --git a/src/components/V2Dropdown/V2Dropdown.test.js b/src/components/V2Dropdown/V2Dropdown.test.js
new file mode 100644
index 0000000..479907c
--- /dev/null
+++ b/src/components/V2Dropdown/V2Dropdown.test.js
@@ -0,0 +1,79 @@
+import React from 'react';
+import { fireEvent, render } from '@testing-library/react';
+
+import { V2Dropdown } from './V2Dropdown';
+import { InputListItem } from '../InputList';
+
+const renderComponent = (props) => {
+ return render();
+};
+
+describe('V2Dropdown', () => {
+ test('forwards className', () => {
+ const { getByTestId } = renderComponent({
+ className: 'myClass',
+ value: '1',
+ label: 'Dropdown',
+ children: [
+ ,
+ ,
+ ],
+ });
+ const component = getByTestId('component');
+ expect(component.classList.contains('myClass')).toBe(true);
+ });
+ test('able to open', () => {
+ const { getByTestId } = renderComponent({
+ value: '1',
+ label: 'Dropdown',
+ children: [
+ ,
+ ,
+ ],
+ });
+ const component = getByTestId('component');
+ fireEvent.click(component);
+ expect(component.classList.contains('is-open')).toBe(true);
+ fireEvent.click(component);
+ expect(component.classList.contains('is-open')).toBe(false);
+ });
+ test('properly sets value', () => {
+ const { getAllByText } = renderComponent({
+ value: '1',
+ label: 'Dropdown',
+ children: [
+
+ Item Foo
+ ,
+
+ Item Bar
+ ,
+ ],
+ });
+ const itemFoo = getAllByText('Item Foo');
+ const itemBar = getAllByText('Item Bar');
+ expect(itemFoo).toHaveLength(2);
+ expect(itemBar).toHaveLength(1);
+ });
+ test('properly propagates onChange', () => {
+ const onChange = jest.fn();
+ const { getByText, getByTestId } = renderComponent({
+ onChange,
+ value: '1',
+ label: 'Dropdown',
+ children: [
+
+ Item Foo
+ ,
+
+ Item Bar
+ ,
+ ],
+ });
+ const component = getByTestId('component');
+ fireEvent.click(component);
+ const itemBar = getByText('Item Bar');
+ fireEvent.click(itemBar);
+ expect(onChange).toHaveBeenCalledWith('2', expect.anything());
+ });
+});
diff --git a/src/components/V2Dropdown/index.js b/src/components/V2Dropdown/index.js
new file mode 100644
index 0000000..da94c9b
--- /dev/null
+++ b/src/components/V2Dropdown/index.js
@@ -0,0 +1 @@
+export * from './V2Dropdown';
diff --git a/src/index.js b/src/index.js
index 2a3a4d9..82aab99 100644
--- a/src/index.js
+++ b/src/index.js
@@ -22,6 +22,8 @@ export * from './components/V2Tabs';
export * from './components/Tag';
export * from './components/Tooltip';
export * from './components/Pagination';
+export * from './components/V2Dropdown';
+export * from './components/InputList';
// content
export * from './content/BasicDataCards';