diff --git a/core/components/css-utilities/Animation/Animation.story.tsx b/core/components/css-utilities/Animation/Animation.story.tsx index 1fc963049d..69a6f35328 100644 --- a/core/components/css-utilities/Animation/Animation.story.tsx +++ b/core/components/css-utilities/Animation/Animation.story.tsx @@ -67,6 +67,14 @@ export const animation = () => { className: 'slideIn-right', properties: 'The object is moving 16px from left to right while fading in, using an entrance-expressive-curve', }, + { + className: 'rotate-clockwise', + properties: 'The object is rotating in a clockwise direction, using a standard-productive-curve', + }, + { + className: 'rotate-anticlockwise', + properties: 'The object is rotating in an anti-clockwise direction, using a standard-productive-curve', + }, ]; return ( diff --git a/core/components/organisms/listbox/Listbox.tsx b/core/components/organisms/listbox/Listbox.tsx new file mode 100644 index 0000000000..ccbec17562 --- /dev/null +++ b/core/components/organisms/listbox/Listbox.tsx @@ -0,0 +1,90 @@ +import * as React from 'react'; +import { BaseProps, extractBaseProps, BaseHtmlProps } from '@/utils/types'; +import classNames from 'classnames'; +import { DraggableList } from './reorderList'; +import { ListboxItem } from './listboxItem'; + +type ListboxType = 'option' | 'description' | 'resource'; +type ListboxSize = 'standard' | 'compressed' | 'tight'; +export type TagType = 'ul' | 'ol' | 'div' | 'nav'; + +export interface ListboxProps extends BaseProps, BaseHtmlProps { + /** + * React Element to be added inside `list` + */ + children: React.ReactNode; + /** + * List size + */ + size: ListboxSize; + /** + * Type of List + */ + type: ListboxType; + /** + * Allows list item re-ordering + */ + draggable?: boolean; + /** + * Set a custom element for Listbox + */ + tagName: TagType; + /** + * Add divider below all list item + */ + showDivider: boolean; +} + +export const ListboxContext = React.createContext>({ + size: 'standard', + type: 'resource', + draggable: false, + showDivider: true, +}); + +const { Provider } = ListboxContext; + +export const Listbox = (props: ListboxProps) => { + const { children, className, draggable, size, type, showDivider, tagName: Tag, ...rest } = props; + const baseProps = extractBaseProps(props); + + const classes = classNames( + { + Listbox: true, + }, + className + ); + + const sharedProp = { + size, + type, + draggable, + showDivider, + }; + + return ( + + {draggable ? ( + + ) : ( + + {children} + + )} + + ); +}; + +Listbox.displayName = 'Listbox'; + +Listbox.defaultProps = { + tagName: 'ul', + size: 'standard', + type: 'resource', + draggable: false, + showDivider: true, +}; + +Listbox.Item = ListboxItem; + +export default Listbox; diff --git a/core/components/organisms/listbox/__stories__/index.story.jsx b/core/components/organisms/listbox/__stories__/index.story.jsx new file mode 100644 index 0000000000..90265e016d --- /dev/null +++ b/core/components/organisms/listbox/__stories__/index.story.jsx @@ -0,0 +1,81 @@ +import * as React from 'react'; +import { Listbox, Heading } from '@/index'; +import { ListboxItem } from '../listboxItem'; + +export const all = () => { + const data = [ + { + assessment: 'Alcohol Usage Disorders Identification Test - C (Audit C)', + }, + { + assessment: 'Functional Assessment - Initial', + }, + { + assessment: 'Functional Assessment - Discharge', + }, + { + assessment: 'Hypertension - Diabetes Symptoms Identification Test', + }, + { + assessment: 'Patient Health Question', + }, + ]; + + return ( +
+ + Select Assessment + + + {data.map((item, key) => { + return {item.assessment}; + })} + +
+ ); +}; + +const customCode = `() => { + const data = [ + { + assessment: 'Alcohol Usage Disorders Identification Test - C (Audit C)', + }, + { + assessment: 'Functional Assessment - Initial', + }, + { + assessment: 'Functional Assessment - Discharge', + }, + { + assessment: 'Hypertension - Diabetes Symptoms Identification Test', + }, + { + assessment: 'Patient Health Question', + }, + ]; + + return ( +
+ Select Assessment + + {data.map((item, key) => { + return {item.assessment}; + })} + +
+ ); +}`; + +export default { + title: 'Layout/Listbox/All', + component: Listbox, + subcomponents: { Listbox, ListboxItem }, + parameters: { + docs: { + docPage: { + customCode, + title: 'Listbox', + }, + }, + }, +}; diff --git a/core/components/organisms/listbox/__stories__/nestedList.story.jsx b/core/components/organisms/listbox/__stories__/nestedList.story.jsx new file mode 100644 index 0000000000..7cd96efef1 --- /dev/null +++ b/core/components/organisms/listbox/__stories__/nestedList.story.jsx @@ -0,0 +1,255 @@ +import * as React from 'react'; +import { Listbox, Card, Text, Icon, MetaList, StatusHint, Switch, Dropdown, Button } from '@/index'; +import { ListboxItem } from '../listboxItem'; +import './style.css'; + +const SubList = () => { + const noteList = ['Call Note', 'Visit note', 'Generic note', 'Ad-hoc task']; + + /* + // style.css + .SubList-wrapper { + margin-left: 40px; + background-color: #fcfafa; + } + */ + + return ( + + {noteList.map((note, key) => { + return ( + + {note} + + + ); + })} + + ); +}; + +export const nestedList = () => { + const [expandList, setExpandList] = React.useState([]); + + const dataList = [ + { + title: 'Provider Chat', + date: '09/11/2022', + user: 'Erin Boor', + status: 'Active', + }, + { + title: 'Healthwise', + date: '09/11/2003', + user: 'Sam', + status: 'Active', + }, + { + title: 'Productivity Metrics', + date: '09/11/2003', + user: 'Eric Sam', + status: 'Active', + }, + { + title: 'New button', + date: '09/11/2003', + user: 'Ashley Conner', + status: 'Active', + }, + { + title: 'Timer', + date: '09/11/2003', + user: 'John Doe', + status: 'Active', + }, + ]; + + const onClickHandler = (key) => { + if (expandList.includes(key)) { + setExpandList(expandList.filter((id) => id !== key)); + } else { + let newOpen = [...expandList]; + newOpen.push(key); + setExpandList(newOpen); + } + }; + + return ( + + + {dataList.map((record, key) => { + const expanded = expandList.includes(key); + return ( + }> + onClickHandler(key)} + appearance="subtle" + className={`mr-4 cursor-pointer ${expanded ? 'rotate-clockwise' : 'rotate-anticlockwise'}`} + /> + +
+
+ {record.title}
+ +
+ +
+ {record.status} +
+
+
+ ); + })} +
+
+ ); +}; + +const customCode = ` +() => { + const SubList = () => { + const noteList = ['Call Note', 'Visit note', 'Generic note', 'Ad-hoc task']; + + /* + // style.css + .SubList-wrapper { + margin-left: 40px; + background-color: #fcfafa; + } + + .Listbox-wrapper { + height: var(--spacing-9); + } + */ + + return ( + + {noteList.map((note, key) => { + return ( + + {note} + + + ); + })} + + ); + }; + + const [expandList, setExpandList] = React.useState([]); + + const dataList = [ + { + title: 'Provider Chat', + date: '09/11/2022', + user: 'Erin Boor', + status: 'Active', + }, + { + title: 'Healthwise', + date: '09/11/2003', + user: 'Sam', + status: 'Active', + }, + { + title: 'Productivity Metrics', + date: '09/11/2003', + user: 'Eric Sam', + status: 'Active', + }, + { + title: 'New button', + date: '09/11/2003', + user: 'Ashley Conner', + status: 'Active', + }, + { + title: 'Timer', + date: '09/11/2003', + user: 'John Doe', + status: 'Active', + }, + ]; + + const onClickHandler = (key) => { + if (expandList.includes(key)) { + setExpandList(expandList.filter((sid) => sid !== key)); + } else { + let newOpen = [...expandList]; + newOpen.push(key); + setExpandList(newOpen); + } + }; + + return ( + + + {dataList.map((record, key) => { + const expanded = expandList.includes(key); + + return ( + }> + onClickHandler(key)} + appearance="subtle" + className={\`mr-4 cursor-pointer \${expanded ? 'rotate-clockwise' : 'rotate-anticlockwise'}\`} + /> + +
+
+ {record.title}
+ +
+ +
+ {record.status} +
+
+
+ ); + })} +
+
+ ); +} +`; + +export default { + title: 'Layout/Listbox/Nested List', + component: Listbox, + subcomponents: { Listbox, ListboxItem }, + parameters: { + docs: { + docPage: { + customCode, + title: 'Listbox', + }, + }, + }, +}; diff --git a/core/components/organisms/listbox/__stories__/reorderList.story.jsx b/core/components/organisms/listbox/__stories__/reorderList.story.jsx new file mode 100644 index 0000000000..5ccab995ca --- /dev/null +++ b/core/components/organisms/listbox/__stories__/reorderList.story.jsx @@ -0,0 +1,94 @@ +import * as React from 'react'; +import { Listbox, Card, CardFooter, Button, Text, Checkbox, Heading, Divider } from '@/index'; +import { ListboxItem } from '../listboxItem'; +import './style.css'; + +export const reorderList = () => { + const dataList = [ + { + name: 'Priority', + checked: true, + }, + { + name: 'Scheduled', + checked: true, + }, + { + name: 'Patient', + checked: false, + }, + { + name: 'Activity details', + checked: true, + }, + { + name: 'Note', + checked: true, + }, + { + name: 'Care gaps', + checked: false, + }, + { + name: 'HHS', + checked: true, + }, + { + name: 'CDPS', + checked: true, + }, + { + name: 'Patient', + checked: false, + }, + ]; + + return ( + // style.css + // .Listbox-wrapper { + // height: var(--spacing-9); + // } + + +
+ Todo’s table columns + Select the columns that you want to see in work list +
+ + + {dataList.map((record, key) => { + return ( + +
+ {record.name} + +
+
+ ); + })} +
+ + + <> + + + + +
+ ); +}; + +export default { + title: 'Layout/Listbox/Reorder List', + component: Listbox, + subcomponents: { Listbox, ListboxItem }, + parameters: { + docs: { + docPage: { + title: 'Listbox', + }, + }, + }, +}; diff --git a/core/components/organisms/listbox/__stories__/sizes/compressed.story.jsx b/core/components/organisms/listbox/__stories__/sizes/compressed.story.jsx new file mode 100644 index 0000000000..e1ca17be0b --- /dev/null +++ b/core/components/organisms/listbox/__stories__/sizes/compressed.story.jsx @@ -0,0 +1,73 @@ +import * as React from 'react'; +import { Listbox, Card, Text, Icon, MetaList, Badge, CardHeader, Heading } from '@/index'; +import { ListboxItem } from '../../listboxItem'; + +export const compressedList = () => { + const dataList = [ + { + title: 'Social Needs Assessment', + time: '2 days ago', + sender: 'Dr. John Matthews(your primary care physician)', + status: 'Due', + }, + { + title: 'Diabetes Self Management Assessment', + time: '1 week ago', + sender: 'Dr. John Matthews(your primary care physician)', + }, + { + title: 'Depression Screening', + time: '2 week ago', + sender: 'Dr. Nina Locke(Psychologist)', + }, + { + title: 'PHQ-9 ', + time: '1 mon ago', + sender: 'Dr. Jimmy', + }, + ]; + + return ( + + + Screenings and assessments + + + {dataList.map((record, key) => { + return ( + + +
+
+
+ {record.title} + {record.status && ( + + {record.status} + + )} +
+ +
+ +
+
+ ); + })} +
+
+ ); +}; + +export default { + title: 'Layout/Listbox/Size/Compressed List', + component: Listbox, + subcomponents: { Listbox, ListboxItem }, + parameters: { + docs: { + docPage: { + title: 'Listbox', + }, + }, + }, +}; diff --git a/core/components/organisms/listbox/__stories__/sizes/standard.story.jsx b/core/components/organisms/listbox/__stories__/sizes/standard.story.jsx new file mode 100644 index 0000000000..7f8944faeb --- /dev/null +++ b/core/components/organisms/listbox/__stories__/sizes/standard.story.jsx @@ -0,0 +1,73 @@ +import * as React from 'react'; +import { Listbox, Card, Text, Icon, MetaList, Badge, CardHeader, Heading } from '@/index'; +import { ListboxItem } from '../../listboxItem'; + +export const standardList = () => { + const dataList = [ + { + title: 'Social Needs Assessment', + time: '2 days ago', + sender: 'Dr. John Matthews(your primary care physician)', + status: 'Due', + }, + { + title: 'Diabetes Self Management Assessment', + time: '1 week ago', + sender: 'Dr. John Matthews(your primary care physician)', + }, + { + title: 'Depression Screening', + time: '2 week ago', + sender: 'Dr. Nina Locke(Psychologist)', + }, + { + title: 'PHQ-9 ', + time: '1 mon ago', + sender: 'Dr. Jimmy', + }, + ]; + + return ( + + + Screenings and assessments + + + {dataList.map((record, key) => { + return ( + + +
+
+
+ {record.title} + {record.status && ( + + {record.status} + + )} +
+ +
+ +
+
+ ); + })} +
+
+ ); +}; + +export default { + title: 'Layout/Listbox/Size/Standard List', + component: Listbox, + subcomponents: { Listbox, ListboxItem }, + parameters: { + docs: { + docPage: { + title: 'Listbox', + }, + }, + }, +}; diff --git a/core/components/organisms/listbox/__stories__/sizes/tight.story.jsx b/core/components/organisms/listbox/__stories__/sizes/tight.story.jsx new file mode 100644 index 0000000000..4962131114 --- /dev/null +++ b/core/components/organisms/listbox/__stories__/sizes/tight.story.jsx @@ -0,0 +1,73 @@ +import * as React from 'react'; +import { Listbox, Card, Text, Icon, MetaList, Badge, CardHeader, Heading } from '@/index'; +import { ListboxItem } from '../../listboxItem'; + +export const tightList = () => { + const dataList = [ + { + title: 'Social Needs Assessment', + time: '2 days ago', + sender: 'Dr. John Matthews(your primary care physician)', + status: 'Due', + }, + { + title: 'Diabetes Self Management Assessment', + time: '1 week ago', + sender: 'Dr. John Matthews(your primary care physician)', + }, + { + title: 'Depression Screening', + time: '2 week ago', + sender: 'Dr. Nina Locke(Psychologist)', + }, + { + title: 'PHQ-9 ', + time: '1 mon ago', + sender: 'Dr. Jimmy', + }, + ]; + + return ( + + + Screenings and assessments + + + {dataList.map((record, key) => { + return ( + + +
+
+
+ {record.title} + {record.status && ( + + {record.status} + + )} +
+ +
+ +
+
+ ); + })} +
+
+ ); +}; + +export default { + title: 'Layout/Listbox/Size/Tight List', + component: Listbox, + subcomponents: { Listbox, ListboxItem }, + parameters: { + docs: { + docPage: { + title: 'Listbox', + }, + }, + }, +}; diff --git a/core/components/organisms/listbox/__stories__/style.css b/core/components/organisms/listbox/__stories__/style.css new file mode 100644 index 0000000000..7ed01c256f --- /dev/null +++ b/core/components/organisms/listbox/__stories__/style.css @@ -0,0 +1,8 @@ +.SubList-wrapper { + margin-left: 40px; + background-color: #fcfafa; +} + +.Listbox-wrapper { + height: var(--spacing-9); +} diff --git a/core/components/organisms/listbox/__stories__/types/descriptionList.story.jsx b/core/components/organisms/listbox/__stories__/types/descriptionList.story.jsx new file mode 100644 index 0000000000..ccc5f85607 --- /dev/null +++ b/core/components/organisms/listbox/__stories__/types/descriptionList.story.jsx @@ -0,0 +1,137 @@ +import * as React from 'react'; +import { Listbox, Card, Heading, CardHeader, Input, CardBody, Text, Divider, Avatar, Button } from '@/index'; +import { ListboxItem } from '../../listboxItem'; +import '../style.css'; + +export const descriptionList = () => { + const dataList = [ + { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@gmail.com', + permission: 'Owner', + }, + { + firstName: 'Nina', + lastName: 'Locke', + email: 'ninalocke@gmail.com', + permission: 'Can edit', + }, + { + firstName: 'Katty', + lastName: 'Perry', + email: 'kattyperry@gmail.com', + permission: 'Can edit', + }, + { + firstName: 'Joy', + lastName: 'Lawson', + email: 'joylawson@gmail.com', + permission: 'Can view', + }, + { + firstName: 'Randy', + lastName: 'Boatwright', + email: 'rboatwright3@arstechnica.com', + permission: 'Can edit', + }, + { + firstName: 'Rolando', + lastName: 'Cyples', + email: 'rcyples4@biglobe.ne.jp', + permission: 'Can edit', + }, + { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@gmail.com', + permission: 'Owner', + }, + { + firstName: 'Nina', + lastName: 'Locke', + email: 'ninalocke@gmail.com', + permission: 'Can edit', + }, + { + firstName: 'Katty', + lastName: 'Perry', + email: 'kattyperry@gmail.com', + permission: 'Can edit', + }, + { + firstName: 'Joy', + lastName: 'Lawson', + email: 'joylawson@gmail.com', + permission: 'Can view', + }, + { + firstName: 'Randy', + lastName: 'Boatwright', + email: 'rboatwright3@arstechnica.com', + permission: 'Can edit', + }, + { + firstName: 'Rolando', + lastName: 'Cyples', + email: 'rcyples4@biglobe.ne.jp', + permission: 'Can edit', + }, + ]; + + return ( + // style.css + // .Listbox-wrapper { + // height: var(--spacing-9); + // } + + + Sharing test manual + + Showing 10 items + + + + + + {dataList.map((data, key) => { + return ( + +
+ +
+ {data.firstName + ' ' + data.lastName}
+ + {data.email} + +
+
+ +
+ ); + })} +
+
+
+ ); +}; + +export default { + title: 'Layout/Listbox/Type/Description List', + component: Listbox, + subcomponents: { Listbox, ListboxItem }, + parameters: { + docs: { + docPage: { + title: 'Listbox', + }, + }, + }, +}; diff --git a/core/components/organisms/listbox/__stories__/types/optionList.story.jsx b/core/components/organisms/listbox/__stories__/types/optionList.story.jsx new file mode 100644 index 0000000000..a3783d4853 --- /dev/null +++ b/core/components/organisms/listbox/__stories__/types/optionList.story.jsx @@ -0,0 +1,112 @@ +import * as React from 'react'; +import { + Listbox, + Card, + Heading, + CardHeader, + Input, + Dropdown, + CardBody, + Tabs, + Tab, + Text, + Icon, + CardFooter, +} from '@/index'; +import { ListboxItem } from '../../listboxItem'; +import '../style.css'; + +export const optionList = () => { + const dataList = [ + { + name: 'ED Discharge care Protocol', + subInfo: 'Pediatric', + }, + { + name: 'Behavioral Outreach', + subInfo: 'Remote patient monitoring', + }, + { + name: 'Chronic Care Protocol - Diabetes', + subInfo: 'Hospice', + }, + { + name: 'TCM Care Protocol', + subInfo: 'Chronic Care Management', + }, + { + name: 'CAD Outreach', + subInfo: 'Pharmacy management', + }, + { + name: 'Diabetes care protocol', + subInfo: 'Remote patient monitoring', + }, + ]; + return ( + // style.css + // .Listbox-wrapper { + // height: var(--spacing-9); + // } + + + + Select starting template + + +
+ + +
+ + + + {dataList.map((data, key) => { + return ( + +
+ {data.name} +
+ {data.subInfo} +
+
+ ); + })} +
+
+ +
Your Protocol
+
+
+
+ + + + Create from scratch + + +
+ ); +}; + +export default { + title: 'Layout/Listbox/Type/Option List', + component: Listbox, + subcomponents: { Listbox, ListboxItem }, + parameters: { + docs: { + docPage: { + title: 'Listbox', + }, + }, + }, +}; diff --git a/core/components/organisms/listbox/__stories__/types/resourceList.story.jsx b/core/components/organisms/listbox/__stories__/types/resourceList.story.jsx new file mode 100644 index 0000000000..b7f9f286b3 --- /dev/null +++ b/core/components/organisms/listbox/__stories__/types/resourceList.story.jsx @@ -0,0 +1,74 @@ +import * as React from 'react'; +import { Listbox, Card, Text, Icon, MetaList, Badge, CardHeader, Heading } from '@/index'; +import { ListboxItem } from '../../listboxItem'; + +export const resourceList = () => { + const dataList = [ + { + title: 'Social Needs Assessment', + time: '2 days ago', + sender: 'Dr. John Matthews(your primary care physician)', + status: 'Due', + }, + { + title: 'Diabetes Self Management Assessment', + time: '1 week ago', + sender: 'Dr. John Matthews(your primary care physician)', + }, + { + title: 'Depression Screening', + time: '2 week ago', + sender: 'Dr. Nina Locke(Psychologist)', + }, + { + title: 'PHQ-9 ', + time: '1 mon ago', + sender: 'Dr. Jimmy', + }, + ]; + + return ( + + + Screenings and assessments + + + + {dataList.map((record, key) => { + return ( + +
+ +
+
+ {record.title} + {record.status && ( + + {record.status} + + )} +
+ +
+
+ +
+ ); + })} +
+
+ ); +}; + +export default { + title: 'Layout/Listbox/Type/Resource List', + component: Listbox, + subcomponents: { Listbox, ListboxItem }, + parameters: { + docs: { + docPage: { + title: 'Listbox', + }, + }, + }, +}; diff --git a/core/components/organisms/listbox/__tests__/Listbox.test.tsx b/core/components/organisms/listbox/__tests__/Listbox.test.tsx new file mode 100644 index 0000000000..0ed1d1dafb --- /dev/null +++ b/core/components/organisms/listbox/__tests__/Listbox.test.tsx @@ -0,0 +1,445 @@ +import * as React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { Listbox, Text } from '@/index'; +import { testHelper, filterUndefined, valueHelper, testMessageHelper } from '@/utils/testHelper'; +import { ListboxItemProps as Props, ListboxProps as ListboxProps } from '@/index.type'; +import { TagType } from '../Listbox'; +import { ItemTagType } from '../listboxItem'; + +const BooleanValue = [true, false]; +const listSize = ['standard', 'compressed', 'tight']; +const listType = ['option', 'description', 'resource']; +const nestedRowElement =
nested row
; +const children =
list item element
; +const listItemId = 'list-item-id'; +const tagList: TagType[] = ['ol', 'ul', 'div', 'nav']; +const listItemTagList: ItemTagType[] = ['li', 'div']; +const onClickHandler = jest.fn(); + +describe('Listbox component snapshot', () => { + const mapper: Record = { + draggable: valueHelper(BooleanValue, { required: true, iterate: true }), + size: valueHelper(listSize, { required: true, iterate: true }), + type: valueHelper(listType, { required: true, iterate: true }), + tagName: valueHelper(tagList, { required: true, iterate: true }), + showDivider: valueHelper(BooleanValue, { required: true, iterate: true }), + }; + + const testFunc = (props: Record): void => { + const attr = filterUndefined(props) as ListboxProps; + + it(testMessageHelper(attr), () => { + const { baseElement } = render( + + {children} + + ); + expect(baseElement).toMatchSnapshot(); + }); + }; + + testHelper(mapper, testFunc); +}); + +describe('Listbox item component snapshot', () => { + const mapper: Record = { + disabled: valueHelper(BooleanValue, { required: true, iterate: true }), + tagName: valueHelper(listItemTagList, { required: true, iterate: true }), + onClick: valueHelper(onClickHandler, { required: true, iterate: false }), + }; + + const testFunc = (props: Record): void => { + const attr = filterUndefined(props) as Props; + + it(testMessageHelper(attr), () => { + const { baseElement } = render( + + {children} + + ); + expect(baseElement).toMatchSnapshot(); + }); + }; + + testHelper(mapper, testFunc); +}); + +describe('Listbox component snapshot for states', () => { + const mapper: Record = { + selected: valueHelper(BooleanValue, { required: true, iterate: true }), + activated: valueHelper(BooleanValue, { required: true, iterate: true }), + type: valueHelper(listType, { required: true, iterate: true }), + }; + + const testFunc = (props: Record): void => { + const attr = filterUndefined(props) as Props; + + it(testMessageHelper(attr), () => { + const { baseElement } = render( + + + {children} + + + ); + expect(baseElement).toMatchSnapshot(); + }); + }; + + testHelper(mapper, testFunc); +}); + +describe('Listbox Item component test for list size: standard', () => { + it('check for padding classes for size:standard', () => { + const { getByTestId } = render( + + {children} + + ); + const listItem = getByTestId('DesignSystem-Listbox-ItemWrapper'); + expect(listItem).toHaveClass('Listbox-item--standard'); + }); +}); + +describe('Listbox Item component test for list size: tight', () => { + it('check for padding classes for size:tight', () => { + const { getByTestId } = render( + + {children} + + ); + const listItem = getByTestId('DesignSystem-Listbox-ItemWrapper'); + expect(listItem).toHaveClass('Listbox-item--tight'); + }); +}); + +describe('Listbox Item component test for list size: compressed', () => { + it('check for padding classes for size:compressed', () => { + const { getByTestId } = render( + + {children} + + ); + const listItem = getByTestId('DesignSystem-Listbox-ItemWrapper'); + expect(listItem).toHaveClass('Listbox-item--compressed'); + }); +}); + +describe('Listbox Item component test for list type: option', () => { + it('check for classes for type:option', () => { + const { getByTestId } = render( + + + {children} + + + ); + const listItem = getByTestId('DesignSystem-Listbox-ItemWrapper'); + expect(listItem).toHaveClass('Listbox-item--selected'); + expect(listItem).toHaveClass('Listbox-item'); + }); +}); + +describe('Listbox Item component test for list type: resource', () => { + it('check for classes for type:resource', () => { + const { getByTestId } = render( + + + {children} + + + ); + const listItem = getByTestId('DesignSystem-Listbox-ItemWrapper'); + expect(listItem).toHaveClass('Listbox-item--activated'); + expect(listItem).toHaveClass('Listbox-item'); + }); +}); + +describe('Listbox Item component test for list type: description', () => { + it('check for classes for type:description', () => { + const { getByTestId } = render( + + {children} + + ); + const listItem = getByTestId('DesignSystem-Listbox-ItemWrapper'); + expect(listItem).toHaveClass('Listbox-item--description'); + }); +}); + +describe('Listbox Item component test for disabled classes', () => { + const { getByTestId } = render( + + {children} + + ); + const listItem = getByTestId('DesignSystem-Listbox-ItemWrapper'); + expect(listItem).toHaveClass('Listbox-item--disabled'); +}); + +describe('Listbox Item component test for custom classes', () => { + it('check for custom class', () => { + const { getByTestId } = render( + + {children} + + ); + const listItem = getByTestId('DesignSystem-Listbox-ItemWrapper'); + expect(listItem).toHaveClass('custom-class'); + }); + + it('check to override default padding classes', () => { + const { getByTestId } = render( + + {children} + + ); + const listItem = getByTestId('DesignSystem-Listbox-ItemWrapper'); + expect(listItem).toHaveClass('p-0'); + }); +}); + +describe('Listbox Item component test for divider', () => { + it('check for divider present below list item', () => { + const { getAllByTestId } = render( + + {children} + {children} + + ); + const divider = getAllByTestId('DesignSystem-Divider'); + expect(divider).toHaveLength(2); + }); + + it('check for divider not present below list item', () => { + render( + + {children} + + ); + const divider = screen.queryByText('DesignSystem-Divider'); + expect(divider).not.toBeInTheDocument(); + }); +}); + +describe('Listbox component test for custom class', () => { + it('check for custom class in listbox', () => { + const { getByTestId } = render( + + {children} + + ); + expect(getByTestId('DesignSystem-Listbox')).toHaveClass('custom-class'); + }); + + it('check for custom class in listbox item component', () => { + const { getByTestId } = render( + + + {children} + + + ); + expect(getByTestId('DesignSystem-Listbox-ItemWrapper')).toHaveClass('custom-class'); + }); +}); + +describe('Listbox component test for drag icon', () => { + const { getByTestId } = render( + + {children} + + ); + + expect(getByTestId('DesignSystem-Listbox-DragIcon')).toBeInTheDocument(); + expect(getByTestId('DesignSystem-Listbox-DragIcon')).toHaveClass('Listbox-item--drag-icon'); +}); + +describe('Listbox component test for TagName', () => { + tagList.forEach((tag) => { + it(`check for listbox component with ${tag} tagName`, () => { + const { getByTestId } = render({children}); + expect(getByTestId('DesignSystem-Listbox').tagName).toMatch(tag.toUpperCase()); + }); + }); +}); + +describe('Listbox Item component test for TagName', () => { + listItemTagList.forEach((tag) => { + it(`check for listbox component with ${tag} tagName`, () => { + const { getByTestId } = render( + + {children} + + ); + expect(getByTestId('DesignSystem-Listbox-Item').tagName).toMatch(tag.toUpperCase()); + }); + }); +}); + +describe('Listbox component test for reorder list', () => { + const dataList = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5']; + it('test for reorder list keydown event', async () => { + const { getAllByTestId } = render( + + {dataList.map((record, key) => { + return ( + + {record} + + ); + })} + + ); + + expect(getAllByTestId('DesignSystem-Text')[1]).toHaveTextContent('Item 2'); + const sourceElement = getAllByTestId('DesignSystem-Listbox-Item')[0]; + fireEvent.keyDown(sourceElement, { key: 'Tab' }); + fireEvent.keyDown(sourceElement, { which: 32 }); + fireEvent.keyDown(sourceElement, { key: 'ArrowDown' }); + fireEvent.keyDown(getAllByTestId('DesignSystem-Listbox-Item')[1], { which: 32 }); + + await waitFor(() => { + expect(getAllByTestId('DesignSystem-Text')[1]).toHaveTextContent('Item 2'); + }); + }); + + it('test for reorder list mouse move event', async () => { + const { getAllByTestId } = render( + + {dataList.map((record, key) => { + return ( + + {record} + + ); + })} + + ); + + expect(getAllByTestId('DesignSystem-Text')[1]).toHaveTextContent('Item 2'); + + const sourceIcon = getAllByTestId('DesignSystem-Listbox-DragIcon')[0]; + + const mouse = [ + { clientX: 10, clientY: 20 }, + { clientX: 15, clientY: 30 }, + ]; + + fireEvent.mouseDown(sourceIcon, mouse[0]); + fireEvent.mouseMove(sourceIcon, mouse[1]); + fireEvent.mouseUp(sourceIcon); + await waitFor(() => { + expect(getAllByTestId('DesignSystem-Text')[1]).toHaveTextContent('Item 3'); + }); + }); +}); + +describe('Listbox component test for keyboard events', () => { + const dataList = ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5']; + + it('test for keyboard arrow down event', () => { + const { getAllByTestId } = render( + + {dataList.map((record, key) => { + return ( + + {record} + + ); + })} + + ); + + const sourceElement = getAllByTestId('DesignSystem-Listbox-ItemWrapper')[0]; + const targetElement = getAllByTestId('DesignSystem-Listbox-ItemWrapper')[1]; + fireEvent.click(sourceElement); + fireEvent.keyDown(sourceElement, { key: 'ArrowDown' }); + + expect(targetElement).toHaveFocus(); + }); + + it('test for keyboard arrow up event', () => { + const { getAllByTestId } = render( + + {dataList.map((record, key) => { + return ( + + {record} + + ); + })} + + ); + + const sourceElement = getAllByTestId('DesignSystem-Listbox-ItemWrapper')[1]; + const targetElement = getAllByTestId('DesignSystem-Listbox-ItemWrapper')[0]; + fireEvent.click(sourceElement); + fireEvent.keyDown(sourceElement, { key: 'ArrowUp' }); + + expect(targetElement).toHaveFocus(); + }); + + it('test for keyboard arrow down event with disabled item', () => { + const { getAllByTestId } = render( + + {dataList.map((record, key) => { + return ( + + {record} + + ); + })} + + ); + + const sourceElement = getAllByTestId('DesignSystem-Listbox-ItemWrapper')[1]; + const targetElement = getAllByTestId('DesignSystem-Listbox-ItemWrapper')[3]; + fireEvent.click(sourceElement); + fireEvent.keyDown(sourceElement, { key: 'ArrowDown' }); + + expect(targetElement).toHaveFocus(); + }); + + it('test for keyboard arrow up event with disabled item', () => { + const { getAllByTestId } = render( + + {dataList.map((record, key) => { + return ( + + {record} + + ); + })} + + ); + + const sourceElement = getAllByTestId('DesignSystem-Listbox-ItemWrapper')[3]; + const targetElement = getAllByTestId('DesignSystem-Listbox-ItemWrapper')[1]; + fireEvent.click(sourceElement); + fireEvent.keyDown(sourceElement, { key: 'ArrowUp' }); + + expect(targetElement).toHaveFocus(); + }); +}); + +describe('Listbox component test for Nested Row', () => { + it('check for nested list when expanded is true', () => { + const { getByTestId } = render( + + {children} + + ); + expect(getByTestId('DesignSystem-Nested-Row')).toBeInTheDocument(); + }); + + it('check for nested list when expanded is false', () => { + render( + + {children} + + ); + const nestedBody = screen.queryByText('DesignSystem-Nested-Row'); + expect(nestedBody).not.toBeInTheDocument(); + }); +}); diff --git a/core/components/organisms/listbox/__tests__/Utils.test.tsx b/core/components/organisms/listbox/__tests__/Utils.test.tsx new file mode 100644 index 0000000000..280c696dc2 --- /dev/null +++ b/core/components/organisms/listbox/__tests__/Utils.test.tsx @@ -0,0 +1,21 @@ +import { arrayMove, binarySearch } from '../reorderList/utils'; + +const items = [1, 2, 3, 4, 5]; +const values = [100, 200, 300, 400, 500]; + +test('arrayMove', () => { + expect(arrayMove(items, 3, 0)).toEqual([4, 1, 2, 3, 5]); + expect(arrayMove(items, -1, 0)).toEqual([5, 1, 2, 3, 4]); + expect(arrayMove(items, 1, -2)).toEqual([1, 3, 4, 2, 5]); + expect(arrayMove(items, -3, -4)).toEqual([1, 3, 2, 4, 5]); +}); + +test('binarySearch', () => { + expect(binarySearch(values, 50)).toBe(-1); + expect(binarySearch(values, 150)).toBe(0); + expect(binarySearch(values, 250)).toBe(1); + expect(binarySearch(values, 399)).toBe(2); + expect(binarySearch(values, 400)).toBe(2); + expect(binarySearch(values, 401)).toBe(3); + expect(binarySearch(values, 1000)).toBe(4); +}); diff --git a/core/components/organisms/listbox/__tests__/__snapshots__/Listbox.test.tsx.snap b/core/components/organisms/listbox/__tests__/__snapshots__/Listbox.test.tsx.snap new file mode 100644 index 0000000000..369f92caff --- /dev/null +++ b/core/components/organisms/listbox/__tests__/__snapshots__/Listbox.test.tsx.snap @@ -0,0 +1,6313 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Listbox component snapshot + draggable: false, size: "compressed", type: "description", tagName: "div", showDivider: false + 1`] = ` + +
+
+
  • +
    +
    + list item element +
    +
    +
  • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "compressed", type: "description", tagName: "div", showDivider: true + 1`] = ` + +
    +
    +
  • +
    +
    + list item element +
    +
    +
    +
  • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "compressed", type: "description", tagName: "nav", showDivider: false + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "compressed", type: "description", tagName: "nav", showDivider: true + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "compressed", type: "description", tagName: "ol", showDivider: false + 1`] = ` + +
    +
      +
    1. +
      +
      + list item element +
      +
      +
    2. +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "compressed", type: "description", tagName: "ol", showDivider: true + 1`] = ` + +
    +
      +
    1. +
      +
      + list item element +
      +
      +
      +
    2. +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "compressed", type: "description", tagName: "ul", showDivider: false + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "compressed", type: "description", tagName: "ul", showDivider: true + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "compressed", type: "option", tagName: "div", showDivider: false + 1`] = ` + +
    +
    +
  • +
    +
    + list item element +
    +
    +
  • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "compressed", type: "option", tagName: "div", showDivider: true + 1`] = ` + +
    +
    +
  • +
    +
    + list item element +
    +
    +
    +
  • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "compressed", type: "option", tagName: "nav", showDivider: false + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "compressed", type: "option", tagName: "nav", showDivider: true + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "compressed", type: "option", tagName: "ol", showDivider: false + 1`] = ` + +
    +
      +
    1. +
      +
      + list item element +
      +
      +
    2. +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "compressed", type: "option", tagName: "ol", showDivider: true + 1`] = ` + +
    +
      +
    1. +
      +
      + list item element +
      +
      +
      +
    2. +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "compressed", type: "option", tagName: "ul", showDivider: false + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "compressed", type: "option", tagName: "ul", showDivider: true + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "compressed", type: "resource", tagName: "div", showDivider: false + 1`] = ` + +
    +
    +
  • +
    +
    + list item element +
    +
    +
  • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "compressed", type: "resource", tagName: "div", showDivider: true + 1`] = ` + +
    +
    +
  • +
    +
    + list item element +
    +
    +
    +
  • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "compressed", type: "resource", tagName: "nav", showDivider: false + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "compressed", type: "resource", tagName: "nav", showDivider: true + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "compressed", type: "resource", tagName: "ol", showDivider: false + 1`] = ` + +
    +
      +
    1. +
      +
      + list item element +
      +
      +
    2. +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "compressed", type: "resource", tagName: "ol", showDivider: true + 1`] = ` + +
    +
      +
    1. +
      +
      + list item element +
      +
      +
      +
    2. +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "compressed", type: "resource", tagName: "ul", showDivider: false + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "compressed", type: "resource", tagName: "ul", showDivider: true + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "standard", type: "description", tagName: "div", showDivider: false + 1`] = ` + +
    +
    +
  • +
    +
    + list item element +
    +
    +
  • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "standard", type: "description", tagName: "div", showDivider: true + 1`] = ` + +
    +
    +
  • +
    +
    + list item element +
    +
    +
    +
  • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "standard", type: "description", tagName: "nav", showDivider: false + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "standard", type: "description", tagName: "nav", showDivider: true + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "standard", type: "description", tagName: "ol", showDivider: false + 1`] = ` + +
    +
      +
    1. +
      +
      + list item element +
      +
      +
    2. +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "standard", type: "description", tagName: "ol", showDivider: true + 1`] = ` + +
    +
      +
    1. +
      +
      + list item element +
      +
      +
      +
    2. +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "standard", type: "description", tagName: "ul", showDivider: false + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "standard", type: "description", tagName: "ul", showDivider: true + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "standard", type: "option", tagName: "div", showDivider: false + 1`] = ` + +
    +
    +
  • +
    +
    + list item element +
    +
    +
  • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "standard", type: "option", tagName: "div", showDivider: true + 1`] = ` + +
    +
    +
  • +
    +
    + list item element +
    +
    +
    +
  • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "standard", type: "option", tagName: "nav", showDivider: false + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "standard", type: "option", tagName: "nav", showDivider: true + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "standard", type: "option", tagName: "ol", showDivider: false + 1`] = ` + +
    +
      +
    1. +
      +
      + list item element +
      +
      +
    2. +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "standard", type: "option", tagName: "ol", showDivider: true + 1`] = ` + +
    +
      +
    1. +
      +
      + list item element +
      +
      +
      +
    2. +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "standard", type: "option", tagName: "ul", showDivider: false + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "standard", type: "option", tagName: "ul", showDivider: true + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "standard", type: "resource", tagName: "div", showDivider: false + 1`] = ` + +
    +
    +
  • +
    +
    + list item element +
    +
    +
  • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "standard", type: "resource", tagName: "div", showDivider: true + 1`] = ` + +
    +
    +
  • +
    +
    + list item element +
    +
    +
    +
  • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "standard", type: "resource", tagName: "nav", showDivider: false + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "standard", type: "resource", tagName: "nav", showDivider: true + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "standard", type: "resource", tagName: "ol", showDivider: false + 1`] = ` + +
    +
      +
    1. +
      +
      + list item element +
      +
      +
    2. +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "standard", type: "resource", tagName: "ol", showDivider: true + 1`] = ` + +
    +
      +
    1. +
      +
      + list item element +
      +
      +
      +
    2. +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "standard", type: "resource", tagName: "ul", showDivider: false + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "standard", type: "resource", tagName: "ul", showDivider: true + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "tight", type: "description", tagName: "div", showDivider: false + 1`] = ` + +
    +
    +
  • +
    +
    + list item element +
    +
    +
  • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "tight", type: "description", tagName: "div", showDivider: true + 1`] = ` + +
    +
    +
  • +
    +
    + list item element +
    +
    +
    +
  • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "tight", type: "description", tagName: "nav", showDivider: false + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "tight", type: "description", tagName: "nav", showDivider: true + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "tight", type: "description", tagName: "ol", showDivider: false + 1`] = ` + +
    +
      +
    1. +
      +
      + list item element +
      +
      +
    2. +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "tight", type: "description", tagName: "ol", showDivider: true + 1`] = ` + +
    +
      +
    1. +
      +
      + list item element +
      +
      +
      +
    2. +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "tight", type: "description", tagName: "ul", showDivider: false + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "tight", type: "description", tagName: "ul", showDivider: true + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "tight", type: "option", tagName: "div", showDivider: false + 1`] = ` + +
    +
    +
  • +
    +
    + list item element +
    +
    +
  • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "tight", type: "option", tagName: "div", showDivider: true + 1`] = ` + +
    +
    +
  • +
    +
    + list item element +
    +
    +
    +
  • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "tight", type: "option", tagName: "nav", showDivider: false + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "tight", type: "option", tagName: "nav", showDivider: true + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "tight", type: "option", tagName: "ol", showDivider: false + 1`] = ` + +
    +
      +
    1. +
      +
      + list item element +
      +
      +
    2. +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "tight", type: "option", tagName: "ol", showDivider: true + 1`] = ` + +
    +
      +
    1. +
      +
      + list item element +
      +
      +
      +
    2. +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "tight", type: "option", tagName: "ul", showDivider: false + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "tight", type: "option", tagName: "ul", showDivider: true + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "tight", type: "resource", tagName: "div", showDivider: false + 1`] = ` + +
    +
    +
  • +
    +
    + list item element +
    +
    +
  • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "tight", type: "resource", tagName: "div", showDivider: true + 1`] = ` + +
    +
    +
  • +
    +
    + list item element +
    +
    +
    +
  • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "tight", type: "resource", tagName: "nav", showDivider: false + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "tight", type: "resource", tagName: "nav", showDivider: true + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "tight", type: "resource", tagName: "ol", showDivider: false + 1`] = ` + +
    +
      +
    1. +
      +
      + list item element +
      +
      +
    2. +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "tight", type: "resource", tagName: "ol", showDivider: true + 1`] = ` + +
    +
      +
    1. +
      +
      + list item element +
      +
      +
      +
    2. +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "tight", type: "resource", tagName: "ul", showDivider: false + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: false, size: "tight", type: "resource", tagName: "ul", showDivider: true + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "compressed", type: "description", tagName: "div", showDivider: false + 1`] = ` + +
    +
    +
    +
  • +
    + + drag_indicator + +
    + list item element +
    +
    +
  • +
    +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "compressed", type: "description", tagName: "div", showDivider: true + 1`] = ` + +
    +
    +
    +
  • +
    + + drag_indicator + +
    + list item element +
    +
    +
    +
  • +
    +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "compressed", type: "description", tagName: "nav", showDivider: false + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "compressed", type: "description", tagName: "nav", showDivider: true + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "compressed", type: "description", tagName: "ol", showDivider: false + 1`] = ` + +
    +
      +
      +
    1. +
      + + drag_indicator + +
      + list item element +
      +
      +
    2. +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "compressed", type: "description", tagName: "ol", showDivider: true + 1`] = ` + +
    +
      +
      +
    1. +
      + + drag_indicator + +
      + list item element +
      +
      +
      +
    2. +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "compressed", type: "description", tagName: "ul", showDivider: false + 1`] = ` + +
    +
      +
      +
    • +
      + + drag_indicator + +
      + list item element +
      +
      +
    • +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "compressed", type: "description", tagName: "ul", showDivider: true + 1`] = ` + +
    +
      +
      +
    • +
      + + drag_indicator + +
      + list item element +
      +
      +
      +
    • +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "compressed", type: "option", tagName: "div", showDivider: false + 1`] = ` + +
    +
    +
    +
  • +
    + + drag_indicator + +
    + list item element +
    +
    +
  • +
    +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "compressed", type: "option", tagName: "div", showDivider: true + 1`] = ` + +
    +
    +
    +
  • +
    + + drag_indicator + +
    + list item element +
    +
    +
    +
  • +
    +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "compressed", type: "option", tagName: "nav", showDivider: false + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "compressed", type: "option", tagName: "nav", showDivider: true + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "compressed", type: "option", tagName: "ol", showDivider: false + 1`] = ` + +
    +
      +
      +
    1. +
      + + drag_indicator + +
      + list item element +
      +
      +
    2. +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "compressed", type: "option", tagName: "ol", showDivider: true + 1`] = ` + +
    +
      +
      +
    1. +
      + + drag_indicator + +
      + list item element +
      +
      +
      +
    2. +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "compressed", type: "option", tagName: "ul", showDivider: false + 1`] = ` + +
    +
      +
      +
    • +
      + + drag_indicator + +
      + list item element +
      +
      +
    • +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "compressed", type: "option", tagName: "ul", showDivider: true + 1`] = ` + +
    +
      +
      +
    • +
      + + drag_indicator + +
      + list item element +
      +
      +
      +
    • +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "compressed", type: "resource", tagName: "div", showDivider: false + 1`] = ` + +
    +
    +
    +
  • +
    + + drag_indicator + +
    + list item element +
    +
    +
  • +
    +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "compressed", type: "resource", tagName: "div", showDivider: true + 1`] = ` + +
    +
    +
    +
  • +
    + + drag_indicator + +
    + list item element +
    +
    +
    +
  • +
    +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "compressed", type: "resource", tagName: "nav", showDivider: false + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "compressed", type: "resource", tagName: "nav", showDivider: true + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "compressed", type: "resource", tagName: "ol", showDivider: false + 1`] = ` + +
    +
      +
      +
    1. +
      + + drag_indicator + +
      + list item element +
      +
      +
    2. +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "compressed", type: "resource", tagName: "ol", showDivider: true + 1`] = ` + +
    +
      +
      +
    1. +
      + + drag_indicator + +
      + list item element +
      +
      +
      +
    2. +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "compressed", type: "resource", tagName: "ul", showDivider: false + 1`] = ` + +
    +
      +
      +
    • +
      + + drag_indicator + +
      + list item element +
      +
      +
    • +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "compressed", type: "resource", tagName: "ul", showDivider: true + 1`] = ` + +
    +
      +
      +
    • +
      + + drag_indicator + +
      + list item element +
      +
      +
      +
    • +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "standard", type: "description", tagName: "div", showDivider: false + 1`] = ` + +
    +
    +
    +
  • +
    + + drag_indicator + +
    + list item element +
    +
    +
  • +
    +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "standard", type: "description", tagName: "div", showDivider: true + 1`] = ` + +
    +
    +
    +
  • +
    + + drag_indicator + +
    + list item element +
    +
    +
    +
  • +
    +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "standard", type: "description", tagName: "nav", showDivider: false + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "standard", type: "description", tagName: "nav", showDivider: true + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "standard", type: "description", tagName: "ol", showDivider: false + 1`] = ` + +
    +
      +
      +
    1. +
      + + drag_indicator + +
      + list item element +
      +
      +
    2. +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "standard", type: "description", tagName: "ol", showDivider: true + 1`] = ` + +
    +
      +
      +
    1. +
      + + drag_indicator + +
      + list item element +
      +
      +
      +
    2. +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "standard", type: "description", tagName: "ul", showDivider: false + 1`] = ` + +
    +
      +
      +
    • +
      + + drag_indicator + +
      + list item element +
      +
      +
    • +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "standard", type: "description", tagName: "ul", showDivider: true + 1`] = ` + +
    +
      +
      +
    • +
      + + drag_indicator + +
      + list item element +
      +
      +
      +
    • +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "standard", type: "option", tagName: "div", showDivider: false + 1`] = ` + +
    +
    +
    +
  • +
    + + drag_indicator + +
    + list item element +
    +
    +
  • +
    +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "standard", type: "option", tagName: "div", showDivider: true + 1`] = ` + +
    +
    +
    +
  • +
    + + drag_indicator + +
    + list item element +
    +
    +
    +
  • +
    +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "standard", type: "option", tagName: "nav", showDivider: false + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "standard", type: "option", tagName: "nav", showDivider: true + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "standard", type: "option", tagName: "ol", showDivider: false + 1`] = ` + +
    +
      +
      +
    1. +
      + + drag_indicator + +
      + list item element +
      +
      +
    2. +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "standard", type: "option", tagName: "ol", showDivider: true + 1`] = ` + +
    +
  • +
    +
    + list item element +
    +
    +
    +
  • +
    +
    +
      +
      +
    • +
      + + drag_indicator + +
      + list item element +
      +
      +
      +
    • +
      +
    +
    +
    +
      +
      +
    1. +
      + + drag_indicator + +
      + list item element +
      +
      +
      +
    2. +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "standard", type: "option", tagName: "ul", showDivider: false + 1`] = ` + +
    +
      +
      +
    • +
      + + drag_indicator + +
      + list item element +
      +
      +
    • +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "standard", type: "option", tagName: "ul", showDivider: true + 1`] = ` + +
    +
      +
      +
    • +
      + + drag_indicator + +
      + list item element +
      +
      +
      +
    • +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "standard", type: "resource", tagName: "div", showDivider: false + 1`] = ` + +
    +
    +
    +
  • +
    + + drag_indicator + +
    + list item element +
    +
    +
  • +
    +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "standard", type: "resource", tagName: "div", showDivider: true + 1`] = ` + +
    +
    +
    +
  • +
    + + drag_indicator + +
    + list item element +
    +
    +
    +
  • +
    +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "standard", type: "resource", tagName: "nav", showDivider: false + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "standard", type: "resource", tagName: "nav", showDivider: true + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "standard", type: "resource", tagName: "ol", showDivider: false + 1`] = ` + +
    +
      +
      +
    1. +
      + + drag_indicator + +
      + list item element +
      +
      +
    2. +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "standard", type: "resource", tagName: "ol", showDivider: true + 1`] = ` + +
    +
      +
      +
    1. +
      + + drag_indicator + +
      + list item element +
      +
      +
      +
    2. +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "standard", type: "resource", tagName: "ul", showDivider: false + 1`] = ` + +
    +
      +
      +
    • +
      + + drag_indicator + +
      + list item element +
      +
      +
    • +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "standard", type: "resource", tagName: "ul", showDivider: true + 1`] = ` + +
    +
      +
      +
    • +
      + + drag_indicator + +
      + list item element +
      +
      +
      +
    • +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "tight", type: "description", tagName: "div", showDivider: false + 1`] = ` + +
    +
    +
    +
  • +
    + + drag_indicator + +
    + list item element +
    +
    +
  • +
    +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "tight", type: "description", tagName: "div", showDivider: true + 1`] = ` + +
    +
    +
    +
  • +
    + + drag_indicator + +
    + list item element +
    +
    +
    +
  • +
    +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "tight", type: "description", tagName: "nav", showDivider: false + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "tight", type: "description", tagName: "nav", showDivider: true + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "tight", type: "description", tagName: "ol", showDivider: false + 1`] = ` + +
    +
      +
      +
    1. +
      + + drag_indicator + +
      + list item element +
      +
      +
    2. +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "tight", type: "description", tagName: "ol", showDivider: true + 1`] = ` + +
    +
      +
      +
    1. +
      + + drag_indicator + +
      + list item element +
      +
      +
      +
    2. +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "tight", type: "description", tagName: "ul", showDivider: false + 1`] = ` + +
    +
      +
      +
    • +
      + + drag_indicator + +
      + list item element +
      +
      +
    • +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "tight", type: "description", tagName: "ul", showDivider: true + 1`] = ` + +
    +
      +
      +
    • +
      + + drag_indicator + +
      + list item element +
      +
      +
      +
    • +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "tight", type: "option", tagName: "div", showDivider: false + 1`] = ` + +
    +
    +
    +
  • +
    + + drag_indicator + +
    + list item element +
    +
    +
  • +
    +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "tight", type: "option", tagName: "div", showDivider: true + 1`] = ` + +
    +
    +
    +
  • +
    + + drag_indicator + +
    + list item element +
    +
    +
    +
  • +
    +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "tight", type: "option", tagName: "nav", showDivider: false + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "tight", type: "option", tagName: "nav", showDivider: true + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "tight", type: "option", tagName: "ol", showDivider: false + 1`] = ` + +
    +
      +
      +
    1. +
      + + drag_indicator + +
      + list item element +
      +
      +
    2. +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "tight", type: "option", tagName: "ol", showDivider: true + 1`] = ` + +
    +
      +
      +
    1. +
      + + drag_indicator + +
      + list item element +
      +
      +
      +
    2. +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "tight", type: "option", tagName: "ul", showDivider: false + 1`] = ` + +
    +
      +
      +
    • +
      + + drag_indicator + +
      + list item element +
      +
      +
    • +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "tight", type: "option", tagName: "ul", showDivider: true + 1`] = ` + +
    +
      +
      +
    • +
      + + drag_indicator + +
      + list item element +
      +
      +
      +
    • +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "tight", type: "resource", tagName: "div", showDivider: false + 1`] = ` + +
    +
    +
    +
  • +
    + + drag_indicator + +
    + list item element +
    +
    +
  • +
    +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "tight", type: "resource", tagName: "div", showDivider: true + 1`] = ` + +
    +
    +
    +
  • +
    + + drag_indicator + +
    + list item element +
    +
    +
    +
  • +
    +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "tight", type: "resource", tagName: "nav", showDivider: false + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "tight", type: "resource", tagName: "nav", showDivider: true + 1`] = ` + +
    + +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "tight", type: "resource", tagName: "ol", showDivider: false + 1`] = ` + +
    +
      +
      +
    1. +
      + + drag_indicator + +
      + list item element +
      +
      +
    2. +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "tight", type: "resource", tagName: "ol", showDivider: true + 1`] = ` + +
    +
      +
      +
    1. +
      + + drag_indicator + +
      + list item element +
      +
      +
      +
    2. +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "tight", type: "resource", tagName: "ul", showDivider: false + 1`] = ` + +
    +
      +
      +
    • +
      + + drag_indicator + +
      + list item element +
      +
      +
    • +
      +
    +
    + +`; + +exports[`Listbox component snapshot + draggable: true, size: "tight", type: "resource", tagName: "ul", showDivider: true + 1`] = ` + +
    +
      +
      +
    • +
      + + drag_indicator + +
      + list item element +
      +
      +
      +
    • +
      +
    +
    + +`; + +exports[`Listbox component snapshot for states + selected: false, activated: false, type: "description" + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot for states + selected: false, activated: false, type: "option" + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot for states + selected: false, activated: false, type: "resource" + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot for states + selected: false, activated: true, type: "description" + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot for states + selected: false, activated: true, type: "option" + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot for states + selected: false, activated: true, type: "resource" + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot for states + selected: true, activated: false, type: "description" + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot for states + selected: true, activated: false, type: "option" + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot for states + selected: true, activated: false, type: "resource" + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot for states + selected: true, activated: true, type: "description" + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot for states + selected: true, activated: true, type: "option" + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox component snapshot for states + selected: true, activated: true, type: "resource" + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox item component snapshot + disabled: false, tagName: "div", onClick: "[Function]" + 1`] = ` + +
    +
      +
      +
      +
      + list item element +
      +
      +
      +
      +
    +
    + +`; + +exports[`Listbox item component snapshot + disabled: false, tagName: "li", onClick: "[Function]" + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
      +
    • +
    +
    + +`; + +exports[`Listbox item component snapshot + disabled: true, tagName: "div", onClick: "[Function]" + 1`] = ` + +
    +
      +
      +
      +
      + list item element +
      +
      +
      +
      +
    +
    + +`; + +exports[`Listbox item component snapshot + disabled: true, tagName: "li", onClick: "[Function]" + 1`] = ` + +
    +
      +
    • +
      +
      + list item element +
      +
      +
      +
    • +
    +
    + +`; diff --git a/core/components/organisms/listbox/index.tsx b/core/components/organisms/listbox/index.tsx new file mode 100644 index 0000000000..39aec05f08 --- /dev/null +++ b/core/components/organisms/listbox/index.tsx @@ -0,0 +1,3 @@ +export { default } from './Listbox'; +export * from './Listbox'; +export * from './listboxItem'; diff --git a/core/components/organisms/listbox/listboxItem/ListBody.tsx b/core/components/organisms/listbox/listboxItem/ListBody.tsx new file mode 100644 index 0000000000..868ad476f9 --- /dev/null +++ b/core/components/organisms/listbox/listboxItem/ListBody.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import classNames from 'classnames'; +import { Icon } from '@/index'; +import { ListboxItemProps } from './ListboxItem'; +import { ListboxContext } from '../Listbox'; +import { onKeyDown } from '../utils'; + +export const ListBody = (props: ListboxItemProps) => { + const { children, className, disabled, selected, activated } = props; + + const contextProp = React.useContext(ListboxContext); + const { size, type, draggable } = contextProp; + + const itemClass = classNames( + { + 'Listbox-item': true, + [`Listbox-item--${size}`]: size, + [`Listbox-item--${type}`]: type, + 'Listbox-item--disabled': disabled, + 'Listbox-item--selected': selected && type === 'option', + 'Listbox-item--activated': activated && type === 'resource', + }, + className + ); + + return ( +
    + {draggable && ( + + )} + {children} +
    + ); +}; + +export default ListBody; diff --git a/core/components/organisms/listbox/listboxItem/ListboxItem.tsx b/core/components/organisms/listbox/listboxItem/ListboxItem.tsx new file mode 100644 index 0000000000..1da47962ce --- /dev/null +++ b/core/components/organisms/listbox/listboxItem/ListboxItem.tsx @@ -0,0 +1,92 @@ +import * as React from 'react'; +import { BaseProps, BaseHtmlProps } from '@/utils/types'; +import { Divider } from '@/index'; +import { ListboxContext } from '../Listbox'; +import { ListBody } from './ListBody'; +import { NestedList } from '../nestedList'; +import classNames from 'classnames'; + +export type ItemTagType = 'li' | 'div'; + +export interface ListboxItemProps extends BaseProps, BaseHtmlProps { + /** + * React Element to be added inside `list` + */ + children: React.ReactNode; + /** + * Element to be shown inside a nested list + */ + nestedBody?: React.ReactNode; + /** + * Set `true` to show nested row + */ + expanded?: boolean; + /** + * Disables the list item + */ + disabled?: boolean; + /** + * Shows list item in selected state
    + * (works in case of **option** list only) + */ + selected?: boolean; + /** + * Shows list item in activated state
    + * (works in case of **resource** list only) + */ + activated?: boolean; + /** + * Unique ID to list item + */ + id?: string; + /** + * Value for particular list item + */ + value?: string; + /** + * Set a custom element for Listbox + */ + tagName: ItemTagType; + /** + * Handler to be called when `ListboxItem` is clicked + */ + onClick?: (e: React.MouseEvent, id?: string, value?: string) => void; +} + +export const ListboxItem = (props: ListboxItemProps) => { + const { nestedBody, expanded, id, onClick, value, tagName: Tag, ...rest } = props; + + const contextProp = React.useContext(ListboxContext); + const { showDivider, draggable } = contextProp; + + const onClickHandler = (e: React.MouseEvent) => { + onClick && onClick(e, id, value); + }; + + const tagClass = classNames({ + ['Listbox-item-wrapper']: !draggable, + }); + + return ( + + + {nestedBody && } + {showDivider && } + + ); +}; + +ListboxItem.displayName = 'ListboxItem'; +ListboxItem.defaultProps = { + tagName: 'li', +}; + +export default ListboxItem; diff --git a/core/components/organisms/listbox/listboxItem/index.tsx b/core/components/organisms/listbox/listboxItem/index.tsx new file mode 100644 index 0000000000..3b8ad91d2d --- /dev/null +++ b/core/components/organisms/listbox/listboxItem/index.tsx @@ -0,0 +1 @@ +export * from './ListboxItem'; diff --git a/core/components/organisms/listbox/nestedList/Animation.tsx b/core/components/organisms/listbox/nestedList/Animation.tsx new file mode 100644 index 0000000000..439bb94a1b --- /dev/null +++ b/core/components/organisms/listbox/nestedList/Animation.tsx @@ -0,0 +1,32 @@ +export const getAnimationClass = (uniqueKey: string, expanded?: boolean) => { + if (expanded) return `nestedList-open-${uniqueKey} 240ms cubic-bezier(0, 0, 0.38, 0.9)`; + else if (!expanded) return `nestedList-close-${uniqueKey} 160ms cubic-bezier(0.2, 0, 1, 0.9)`; + return ''; +}; + +const getHeight = (listItemRef: React.RefObject) => { + const scrollHeight = listItemRef.current?.scrollHeight; + return scrollHeight; +}; + +export const menuItemAnimation = (listItemRef: React.RefObject, uniqueKey: string) => { + return ` + @keyframes nestedList-open-${uniqueKey} { + from { + height: 0px; + } + to { + height: ${getHeight(listItemRef)}px; + } + } + + @keyframes nestedList-close-${uniqueKey} { + from { + height: ${getHeight(listItemRef)}px; + } + to { + height: 0px; + } + } + `; +}; diff --git a/core/components/organisms/listbox/nestedList/NestedList.tsx b/core/components/organisms/listbox/nestedList/NestedList.tsx new file mode 100644 index 0000000000..a4271086c4 --- /dev/null +++ b/core/components/organisms/listbox/nestedList/NestedList.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; +import { menuItemAnimation, getAnimationClass } from './Animation'; + +export interface NestedListProp { + expanded?: boolean; + nestedBody?: React.ReactNode; +} + +function usePrevious(value?: boolean) { + const ref = React.useRef(); + React.useEffect(() => { + if (value != undefined) { + ref.current = value; + } + }, [value]); + return ref.current; +} + +export const NestedList = (props: NestedListProp) => { + const { nestedBody, expanded } = props; + const prevState = usePrevious(expanded); + const [open, setOpen] = React.useState(expanded); + const [keyframe, setKeyframe] = React.useState(''); + const listItemRef = React.useRef(null); + const uniqueKey = Math.random().toString(36).substring(2, 6); + + const [animation, setAnimation] = React.useState(getAnimationClass(uniqueKey, expanded)); + + React.useEffect(() => { + if (prevState != undefined && prevState !== expanded) { + setOpen(true); + } + requestAnimationFrame(() => { + const result = menuItemAnimation(listItemRef, uniqueKey); + setKeyframe(result); + }); + + const animationClass = getAnimationClass(uniqueKey, expanded); + setAnimation(animationClass); + }, [expanded]); + + const handleAnimationEnd = () => { + !expanded && setOpen(false); + }; + + const styles: React.CSSProperties = { + animation, + overflow: 'hidden', + animationFillMode: 'forwards', + }; + + return ( + <> + + {nestedBody && open && ( +
    + {nestedBody} +
    + )} + + ); +}; + +export default NestedList; diff --git a/core/components/organisms/listbox/nestedList/index.tsx b/core/components/organisms/listbox/nestedList/index.tsx new file mode 100644 index 0000000000..229291987a --- /dev/null +++ b/core/components/organisms/listbox/nestedList/index.tsx @@ -0,0 +1 @@ +export * from './NestedList'; diff --git a/core/components/organisms/listbox/reorderList/Draggable.tsx b/core/components/organisms/listbox/reorderList/Draggable.tsx new file mode 100644 index 0000000000..1acc85ff95 --- /dev/null +++ b/core/components/organisms/listbox/reorderList/Draggable.tsx @@ -0,0 +1,493 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { getTranslateOffset, transformItem, setItemTransition, binarySearch, schd, isTouchEvent } from './utils'; +import type { IItemProps, IProps, TEvent } from './types'; + +const AUTOSCROLL_ACTIVE_OFFSET = 200; +const AUTOSCROLL_SPEED_RATIO = 10; + +class Draggable extends React.Component> { + listRef = React.createRef(); + ghostRef = React.createRef(); + topOffsets: number[] = []; + itemTranslateOffsets: number[] = []; + initialYOffset = 0; + lastScroll = 0; + lastYOffset = 0; + lastListYOffset = 0; + dropTimeout?: number; + needle = -1; + afterIndex = -2; + state = { + itemDragged: -1, + itemDraggedOutOfBounds: -1, + selectedItem: -1, + initialX: 0, + initialY: 0, + targetX: 0, + targetY: 0, + targetHeight: 0, + targetWidth: 0, + scrollingSpeed: 0, + scrollWindow: false, + }; + schdOnMouseMove: { (e: MouseEvent): void; cancel(): void }; + schdOnTouchMove: { (e: TouchEvent): void; cancel(): void }; + schdOnEnd: { (e: Event): void; cancel(): void }; + + constructor(props: IProps) { + super(props); + this.schdOnMouseMove = schd(this.onMouseMove); + this.schdOnTouchMove = schd(this.onTouchMove); + this.schdOnEnd = schd(this.onEnd); + } + + componentDidMount() { + this.calculateOffsets(); + document.addEventListener('touchstart', this.onMouseOrTouchStart as any, { + passive: false, + capture: false, + }); + document.addEventListener('mousedown', this.onMouseOrTouchStart as any); + } + + componentDidUpdate(_prevProps: any, prevState: { scrollingSpeed: number }) { + if (prevState.scrollingSpeed !== this.state.scrollingSpeed && prevState.scrollingSpeed === 0) { + this.doScrolling(); + } + } + + componentWillUnmount() { + document.removeEventListener('touchstart', this.onMouseOrTouchStart as any); + document.removeEventListener('mousedown', this.onMouseOrTouchStart as any); + if (this.dropTimeout) { + window.clearTimeout(this.dropTimeout); + } + this.schdOnMouseMove.cancel(); + this.schdOnTouchMove.cancel(); + this.schdOnEnd.cancel(); + } + + doScrolling = () => { + const { scrollingSpeed, scrollWindow } = this.state; + const listEl = this.listRef.current!; + window.requestAnimationFrame(() => { + if (scrollWindow) { + window.scrollTo(window.pageXOffset, window.pageYOffset + scrollingSpeed * 1.5); + } else { + listEl.scrollTop += scrollingSpeed; + } + if (scrollingSpeed !== 0) { + this.doScrolling(); + } + }); + }; + + getChildren = () => { + if (this.listRef && this.listRef.current) { + return Array.from(this.listRef.current.children); + } + + return []; + }; + + static defaultProps = { + transitionDuration: 240, + lockVertically: false, + removableByMove: false, + }; + + calculateOffsets = () => { + this.topOffsets = this.getChildren().map((item) => item.getBoundingClientRect().top); + this.itemTranslateOffsets = this.getChildren().map((item) => getTranslateOffset(item)); + }; + + getTargetIndex = (e: TEvent) => { + return this.getChildren().findIndex((child) => child === e.target || child.contains(e.target as Node)); + }; + + onMouseOrTouchStart = (e: MouseEvent & TouchEvent) => { + if (this.dropTimeout && this.state.itemDragged > -1) { + window.clearTimeout(this.dropTimeout); + this.finishDrop(); + } + const isTouch = isTouchEvent(e); + if (!isTouch && e.button !== 0) return; + const index = this.getTargetIndex(e as any); + + const listItemTouched = this.getChildren()[index] as HTMLElement; + const isValidDragHandle = (e.target as Element)?.classList.contains('Listbox-item--drag-icon'); + if (!isValidDragHandle) return; + e.preventDefault(); + + if (isTouch) { + const opts = { passive: false }; + listItemTouched.style.touchAction = 'none'; + document.addEventListener('touchend', this.schdOnEnd, opts); + document.addEventListener('touchmove', this.schdOnTouchMove, opts); + document.addEventListener('touchcancel', this.schdOnEnd, opts); + } else { + document.addEventListener('mousemove', this.schdOnMouseMove); + document.addEventListener('mouseup', this.schdOnEnd); + + const listItemDragged = this.getChildren()[this.state.itemDragged] as HTMLElement; + if (listItemDragged && listItemDragged.style) { + listItemDragged.style.touchAction = ''; + } + } + this.onStart( + listItemTouched, + isTouch ? e.touches[0].clientX : e.clientX, + isTouch ? e.touches[0].clientY : e.clientY, + index + ); + }; + + getYOffset = () => { + const listScroll = this.listRef.current ? this.listRef.current.scrollTop : 0; + return window.pageYOffset + listScroll; + }; + + onStart = (target: HTMLElement, clientX: number, clientY: number, index: number) => { + if (this.state.selectedItem > -1) { + this.setState({ selectedItem: -1 }); + this.needle = -1; + } + const targetRect = target.getBoundingClientRect() as DOMRect; + const targetStyles = window.getComputedStyle(target); + this.calculateOffsets(); + this.initialYOffset = this.getYOffset(); + this.lastYOffset = window.pageYOffset; + this.lastListYOffset = this.listRef.current!.scrollTop; + this.setState({ + itemDragged: index, + targetX: targetRect.left - parseInt(targetStyles['margin-left' as any], 10), + targetY: targetRect.top - parseInt(targetStyles['margin-top' as any], 10), + targetHeight: targetRect.height, + targetWidth: targetRect.width, + initialX: clientX, + initialY: clientY, + }); + }; + + onMouseMove = (e: MouseEvent) => { + e.cancelable && e.preventDefault(); + this.onMove(e.clientX, e.clientY); + }; + + onTouchMove = (e: TouchEvent) => { + e.cancelable && e.preventDefault(); + this.onMove(e.touches[0].clientX, e.touches[0].clientY); + }; + + onWheel = (e: React.WheelEvent) => { + if (this.state.itemDragged < 0) return; + this.lastScroll = this.listRef.current!.scrollTop += e.deltaY; + this.moveOtherItems(); + }; + + onMove = (clientX: number, clientY: number) => { + if (this.state.itemDragged === -1) return null; + transformItem( + this.ghostRef.current!, + clientY - this.state.initialY, + this.props.lockVertically ? 0 : clientX - this.state.initialX + ); + this.autoScrolling(clientY); + this.moveOtherItems(); + + return; + }; + + moveOtherItems = () => { + const targetRect = this.ghostRef.current!.getBoundingClientRect(); + const itemVerticalCenter = targetRect.top + targetRect.height / 2; + const offset = getTranslateOffset(this.getChildren()[this.state.itemDragged]); + const currentYOffset = this.getYOffset(); + // adjust offsets if scrolling happens during the item movement + if (this.initialYOffset !== currentYOffset) { + this.topOffsets = this.topOffsets.map((offset) => offset - (currentYOffset - this.initialYOffset)); + this.initialYOffset = currentYOffset; + } + if (this.isDraggedItemOutOfBounds() && this.props.removableByMove) { + this.afterIndex = this.topOffsets.length + 1; + } else { + this.afterIndex = binarySearch(this.topOffsets, itemVerticalCenter); + } + this.animateItems(this.afterIndex === -1 ? 0 : this.afterIndex, this.state.itemDragged, offset); + }; + + autoScrolling = (clientY: number) => { + const { top, bottom, height } = this.listRef.current!.getBoundingClientRect(); + const viewportHeight = window.innerHeight || document.documentElement.clientHeight; + // auto scrolling for the window (down) + if (bottom > viewportHeight && viewportHeight - clientY < AUTOSCROLL_ACTIVE_OFFSET) { + this.setState({ + scrollingSpeed: Math.round((AUTOSCROLL_ACTIVE_OFFSET - (viewportHeight - clientY)) / AUTOSCROLL_SPEED_RATIO), + scrollWindow: true, + }); + // auto scrolling for the window (up) + } else if (top < 0 && clientY < AUTOSCROLL_ACTIVE_OFFSET) { + this.setState({ + scrollingSpeed: Math.round((AUTOSCROLL_ACTIVE_OFFSET - clientY) / -AUTOSCROLL_SPEED_RATIO), + scrollWindow: true, + }); + } else { + if (this.state.scrollWindow && this.state.scrollingSpeed !== 0) { + this.setState({ scrollingSpeed: 0, scrollWindow: false }); + } + // auto scrolling for containers with overflow + if (height + 20 < this.listRef.current!.scrollHeight) { + let scrollingSpeed = 0; + if (clientY - top < AUTOSCROLL_ACTIVE_OFFSET) { + scrollingSpeed = Math.round((AUTOSCROLL_ACTIVE_OFFSET - (clientY - top)) / -AUTOSCROLL_SPEED_RATIO); + } else if (bottom - clientY < AUTOSCROLL_ACTIVE_OFFSET) { + scrollingSpeed = Math.round((AUTOSCROLL_ACTIVE_OFFSET - (bottom - clientY)) / AUTOSCROLL_SPEED_RATIO); + } + if (this.state.scrollingSpeed !== scrollingSpeed) { + this.setState({ scrollingSpeed }); + } + } + } + }; + + animateItems = (needle: number, movedItem: number, offset: number, animateMovedItem = false) => { + this.getChildren().forEach((item, i) => { + setItemTransition(item, this.props.transitionDuration); + if (movedItem === i && animateMovedItem) { + if (movedItem === needle) { + return transformItem(item, null); + } + transformItem( + item, + movedItem < needle + ? this.itemTranslateOffsets.slice(movedItem + 1, needle + 1).reduce((a, b) => a + b, 0) + : this.itemTranslateOffsets.slice(needle, movedItem).reduce((a, b) => a + b, 0) * -1 + ); + } else if (movedItem < needle && i > movedItem && i <= needle) { + transformItem(item, -offset); + } else if (i < movedItem && movedItem > needle && i >= needle) { + transformItem(item, offset); + } else { + transformItem(item, null); + } + }); + }; + + isDraggedItemOutOfBounds = () => { + const initialRect = this.getChildren()[this.state.itemDragged].getBoundingClientRect(); + const targetRect = this.ghostRef.current!.getBoundingClientRect(); + if (Math.abs(initialRect.left - targetRect.left) > targetRect.width) { + if (this.state.itemDraggedOutOfBounds === -1) { + this.setState({ itemDraggedOutOfBounds: this.state.itemDragged }); + } + return true; + } + if (this.state.itemDraggedOutOfBounds > -1) { + this.setState({ itemDraggedOutOfBounds: -1 }); + } + return false; + }; + + onEnd = (e: TouchEvent & MouseEvent) => { + e.cancelable && e.preventDefault(); + document.removeEventListener('mousemove', this.schdOnMouseMove); + document.removeEventListener('touchmove', this.schdOnTouchMove); + document.removeEventListener('mouseup', this.schdOnEnd); + document.removeEventListener('touchup', this.schdOnEnd); + document.removeEventListener('touchcancel', this.schdOnEnd); + + const removeItem = this.props.removableByMove && this.isDraggedItemOutOfBounds(); + if (!removeItem && this.props.transitionDuration > 0 && this.afterIndex !== -2) { + // animate drop + schd(() => { + setItemTransition(this.ghostRef.current!, this.props.transitionDuration, 'cubic-bezier(0.2, 0, 0.38, 0.9)'); + if (this.afterIndex < 1 && this.state.itemDragged === 0) { + transformItem(this.ghostRef.current!, 0, 0); + } else { + transformItem( + this.ghostRef.current!, + // compensate window scroll + -(window.pageYOffset - this.lastYOffset) + + // compensate container scroll + -(this.listRef.current!.scrollTop - this.lastListYOffset) + + (this.state.itemDragged < this.afterIndex + ? this.itemTranslateOffsets + .slice(this.state.itemDragged + 1, this.afterIndex + 1) + .reduce((a, b) => a + b, 0) + : this.itemTranslateOffsets + .slice(this.afterIndex < 0 ? 0 : this.afterIndex, this.state.itemDragged) + .reduce((a, b) => a + b, 0) * -1), + 0 + ); + } + })(); + } + this.dropTimeout = window.setTimeout( + this.finishDrop, + removeItem || this.afterIndex === -2 ? 0 : this.props.transitionDuration + ); + }; + + finishDrop = () => { + const removeItem = this.props.removableByMove && this.isDraggedItemOutOfBounds(); + if (removeItem || (this.afterIndex > -2 && this.state.itemDragged !== this.afterIndex)) { + this.props.onChange({ + oldIndex: this.state.itemDragged, + newIndex: removeItem ? -1 : Math.max(this.afterIndex, 0), + targetRect: this.ghostRef.current!.getBoundingClientRect(), + }); + } + this.getChildren().forEach((item) => { + setItemTransition(item, 0); + transformItem(item, null); + (item as HTMLElement).style.touchAction = ''; + }); + this.setState({ itemDragged: -1, scrollingSpeed: 0 }); + this.afterIndex = -2; + // sometimes the scroll gets messed up after the drop, fix: + if (this.lastScroll > 0) { + this.listRef.current!.scrollTop = this.lastScroll; + this.lastScroll = 0; + } + }; + + onKeyDown = (e: React.KeyboardEvent) => { + const selectedItem = this.state.selectedItem; + const index = this.getTargetIndex(e); + + if (index === -1 || (this.props.values[index] && this.props.values[index].props.disabled)) { + return; + } + + if (e.key === ' ') { + e.preventDefault(); + if (selectedItem === index) { + if (selectedItem !== this.needle) { + this.getChildren().forEach((item) => { + setItemTransition(item, 0); + transformItem(item, null); + }); + this.props.onChange({ + oldIndex: selectedItem, + newIndex: this.needle, + targetRect: this.getChildren()[this.needle].getBoundingClientRect(), + }); + + (this.getChildren()[this.needle] as HTMLElement).focus(); + } + this.setState({ + selectedItem: -1, + }); + this.needle = -1; + } else { + this.setState({ + selectedItem: index, + }); + this.needle = index; + this.calculateOffsets(); + } + } + if ((e.key === 'ArrowDown' || e.key === 'j') && selectedItem > -1 && this.needle < this.props.values.length - 1) { + e.preventDefault(); + const offset = getTranslateOffset(this.getChildren()[selectedItem]); + this.needle++; + this.animateItems(this.needle, selectedItem, offset, true); + } + if ((e.key === 'ArrowUp' || e.key === 'k') && selectedItem > -1 && this.needle > 0) { + e.preventDefault(); + const offset = getTranslateOffset(this.getChildren()[selectedItem]); + this.needle--; + this.animateItems(this.needle, selectedItem, offset, true); + } + if (e.key === 'Escape' && selectedItem > -1) { + this.getChildren().forEach((item) => { + setItemTransition(item, 0); + transformItem(item, null); + }); + this.setState({ + selectedItem: -1, + }); + this.needle = -1; + } + if ((e.key === 'Tab' || e.key === 'Enter') && selectedItem > -1) { + e.preventDefault(); + } + }; + + render() { + const baseStyle = { + userSelect: 'none', + WebkitUserSelect: 'none', + MozUserSelect: 'none', + msUserSelect: 'none', + boxSizing: 'border-box', + position: 'relative', + } as React.CSSProperties; + const ghostStyle = { + ...baseStyle, + top: this.state.targetY, + left: this.state.targetX, + width: this.state.targetWidth, + height: this.state.targetHeight, + backgroundColor: '#ffffff', + listStyleType: 'none', + margin: 0, + position: 'fixed', + boxShadow: '0 4px 16px 0 rgba(0, 0, 0, 0.16)', + } as React.CSSProperties; + return ( + + {this.props.renderList({ + children: this.props.values.map((value: any, index: number) => { + const isHidden = index === this.state.itemDragged; + const isSelected = index === this.state.selectedItem; + + const isDisabled = this.props.values[index] && this.props.values[index].props.disabled; + const props: IItemProps = { + key: index, + tabIndex: isDisabled ? -1 : 0, + onKeyDown: this.onKeyDown, + style: { + ...baseStyle, + visibility: isHidden ? 'hidden' : undefined, + zIndex: isSelected ? 5000 : 0, + } as React.CSSProperties, + }; + + return this.props.renderItem({ + value, + props, + index, + isDragged: false, + isSelected, + isOutOfBounds: false, + }); + }), + isDragged: this.state.itemDragged > -1, + props: { + ref: this.listRef, + }, + })} + {this.state.itemDragged > -1 && + ReactDOM.createPortal( + this.props.renderItem({ + value: this.props.values[this.state.itemDragged], + props: { + ref: this.ghostRef, + style: ghostStyle, + onWheel: this.onWheel, + }, + index: this.state.itemDragged, + isDragged: true, + isSelected: false, + isOutOfBounds: this.state.itemDraggedOutOfBounds > -1, + }), + document.body + )} + + ); + } +} + +export default Draggable; diff --git a/core/components/organisms/listbox/reorderList/DraggableList.tsx b/core/components/organisms/listbox/reorderList/DraggableList.tsx new file mode 100644 index 0000000000..79c3bd669e --- /dev/null +++ b/core/components/organisms/listbox/reorderList/DraggableList.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { extractBaseProps } from '@/utils/types'; +import Draggable from './Draggable'; +import { arrayMove } from './utils'; +import { ListboxProps } from '@/index.type'; +import classNames from 'classnames'; + +export const DraggableList = (props: ListboxProps) => { + const { children, className, tagName: Tag } = props; + const baseProps = extractBaseProps(props); + + const classes = classNames( + { + Listbox: true, + }, + className + ); + + const renderChildren = React.Children.toArray(children).map((child: any) => { + const element = React.cloneElement(child, { parentProps: { ...props } }); + return element; + }); + + const [childList, setChildList] = React.useState(renderChildren); + + const onChangeHandler = (props: any) => { + const { oldIndex, newIndex } = props; + const updatedList = arrayMove(childList, oldIndex, newIndex); + + setChildList(updatedList); + }; + + return ( + { + return ( +
    + {value} +
    + ); + }} + renderList={({ children, props }) => ( + + {children} + + )} + /> + ); +}; diff --git a/core/components/organisms/listbox/reorderList/index.tsx b/core/components/organisms/listbox/reorderList/index.tsx new file mode 100644 index 0000000000..6b01f413eb --- /dev/null +++ b/core/components/organisms/listbox/reorderList/index.tsx @@ -0,0 +1 @@ +export * from './DraggableList'; diff --git a/core/components/organisms/listbox/reorderList/types.ts b/core/components/organisms/listbox/reorderList/types.ts new file mode 100644 index 0000000000..0a366aa755 --- /dev/null +++ b/core/components/organisms/listbox/reorderList/types.ts @@ -0,0 +1,52 @@ +export interface IItemProps { + key?: number; + tabIndex?: number; + 'aria-roledescription'?: string; + onKeyDown?: (e: React.KeyboardEvent) => void; + onWheel?: (e: React.WheelEvent) => void; + style?: React.CSSProperties; + ref?: React.RefObject; +} + +export interface RenderItemParams { + value: Value; + props: IItemProps; + index?: number; + isDragged: boolean; + isSelected: boolean; + isOutOfBounds: boolean; +} + +export interface RenderListParams { + children: React.ReactNode; + isDragged: boolean; + props: { + ref: React.RefObject; + }; +} + +export interface BeforeDragParams { + elements: Element[]; + index: number; +} + +export interface OnChangeMeta { + oldIndex: number; + newIndex: number; + targetRect: ClientRect; +} + +export interface IProps { + beforeDrag?: (params: BeforeDragParams) => void; + renderItem: (params: RenderItemParams) => React.ReactNode; + renderList: (props: RenderListParams) => React.ReactNode; + // values: Value[]; + values: any; + onChange: (meta: OnChangeMeta) => void; + transitionDuration: number; + removableByMove: boolean; + lockVertically: boolean; + container?: Element | null; +} + +export type TEvent = React.MouseEvent | React.TouchEvent | React.KeyboardEvent; diff --git a/core/components/organisms/listbox/reorderList/utils.ts b/core/components/organisms/listbox/reorderList/utils.ts new file mode 100644 index 0000000000..70a1b1974f --- /dev/null +++ b/core/components/organisms/listbox/reorderList/utils.ts @@ -0,0 +1,73 @@ +export function arrayMove(array: T[], from: number, to: number) { + array = array.slice(); + array.splice(to < 0 ? array.length + to : to, 0, array.splice(from, 1)[0]); + return array; +} + +export function getTranslateOffset(element: Element) { + const style = window.getComputedStyle(element); + return ( + Math.max(parseInt(style['margin-top' as any], 10), parseInt(style['margin-bottom' as any], 10)) + + element.getBoundingClientRect().height + ); +} + +export function isTouchEvent(event: TouchEvent & MouseEvent) { + return (event.touches && event.touches.length) || (event.changedTouches && event.changedTouches.length); +} + +export function transformItem(element: Element, offsetY: number | null = 0, offsetX: number | null = 0) { + if (!element) return; + if (offsetY === null || offsetX === null) { + (element as HTMLElement).style.removeProperty('transform'); + return; + } + (element as HTMLElement).style.transform = `translate(${offsetX}px, ${offsetY}px)`; +} + +export function setItemTransition(element: Element, duration: number, timing?: string) { + if (element) { + (element as HTMLElement).style['transition' as any] = `transform ${duration}ms${timing ? ` ${timing}` : ''}`; + } +} + +// returns the "slot" for the targetValue, aka where it should go +// in an ordered "array", it starts with -1 index +export function binarySearch(array: number[], targetValue: number) { + let min = 0; + let max = array.length - 1; + let guess: number; + while (min <= max) { + guess = Math.floor((max + min) / 2); + if (!array[guess + 1] || (array[guess] <= targetValue && array[guess + 1] >= targetValue)) { + return guess; + } else if (array[guess] < targetValue && array[guess + 1] < targetValue) { + min = guess + 1; + } else { + max = guess - 1; + } + } + return -1; +} + +// A throttle function that uses requestAnimationFrame to rate limit +export const schd = (fn: any) => { + let lastArgs: any[] = []; + let frameId: number | null = null; + const wrapperFn = (...args: any[]) => { + lastArgs = args; + if (frameId) { + return; + } + frameId = requestAnimationFrame(() => { + frameId = null; + fn(...lastArgs); + }); + }; + wrapperFn.cancel = () => { + if (frameId) { + cancelAnimationFrame(frameId); + } + }; + return wrapperFn; +}; diff --git a/core/components/organisms/listbox/utils.ts b/core/components/organisms/listbox/utils.ts new file mode 100644 index 0000000000..8fd8289e31 --- /dev/null +++ b/core/components/organisms/listbox/utils.ts @@ -0,0 +1,47 @@ +const isDisabledElement = (element: HTMLElement) => { + return element && element.getAttribute('data-disabled') === 'true'; +}; + +const getNextSibling = (element: HTMLElement) => { + return element?.parentNode?.nextSibling?.firstChild as HTMLElement; +}; + +const getPrevSibling = (element: HTMLElement) => { + return element?.parentNode?.previousSibling?.firstChild as HTMLElement; +}; + +const focusOption = (element: HTMLElement, direction: string) => { + let iterateElement = element; + + while (iterateElement) { + if (!isDisabledElement(iterateElement)) { + iterateElement.focus(); + break; + } + + if (direction === 'down') { + iterateElement = getNextSibling(iterateElement); + } else { + iterateElement = getPrevSibling(iterateElement); + } + } +}; + +export const onKeyDown = (event: React.KeyboardEvent) => { + const sourceElement = event.target as HTMLElement; + const nextElement = getNextSibling(sourceElement); + const prevElement = getPrevSibling(sourceElement); + + switch (event.key) { + case 'ArrowDown': + event.preventDefault(); + focusOption(nextElement, 'down'); + break; + case 'ArrowUp': + event.preventDefault(); + focusOption(prevElement, 'up'); + break; + default: + break; + } +}; diff --git a/core/index.tsx b/core/index.tsx index 5c029f935a..cdf491b4fa 100644 --- a/core/index.tsx +++ b/core/index.tsx @@ -87,5 +87,6 @@ export { HelpText } from './components/atoms/helpText'; export { LinkButton } from './components/atoms/linkButton'; export { ActionCard } from './components/atoms/actionCard'; export { SelectionCard } from './components/atoms/selectionCard'; +export { Listbox } from './components/organisms/listbox'; export { version } from '../package.json'; diff --git a/core/index.type.tsx b/core/index.type.tsx index 510fa00b76..8da01c2002 100644 --- a/core/index.type.tsx +++ b/core/index.type.tsx @@ -84,3 +84,4 @@ export { HelpTextProps } from './components/atoms/helpText'; export { LinkButtonProps } from './components/atoms/linkButton'; export { ActionCardProps } from './components/atoms/actionCard'; export { SelectionCardProps } from './components/atoms/selectionCard'; +export { ListboxProps, ListboxItemProps } from './components/organisms/listbox'; diff --git a/css/src/components/card.css b/css/src/components/card.css index c18db5678b..85b023dbde 100644 --- a/css/src/components/card.css +++ b/css/src/components/card.css @@ -4,7 +4,6 @@ border-radius: var(--spacing-m); border: var(--spacing-xs) solid var(--secondary-light); position: relative; - overflow: hidden; background-color: var(--white); } diff --git a/css/src/components/listbox.css b/css/src/components/listbox.css new file mode 100644 index 0000000000..a18dd8eae3 --- /dev/null +++ b/css/src/components/listbox.css @@ -0,0 +1,115 @@ +.Listbox { + margin: 0; + padding: 0; + display: flex; + list-style-type: none; + flex-direction: column; +} + +.Listbox-item { + display: flex; + align-items: center; + padding-left: var(--spacing-2); + padding-right: var(--spacing-2); +} + +.Listbox-item-wrapper:last-child > .Listbox-divider, +.Listbox-item--draggable:last-child .Listbox-divider { + background: transparent; +} + +/* Sizes */ + +.Listbox-item--tight { + padding-top: var(--spacing-m); + padding-bottom: var(--spacing-m); +} + +.Listbox-item--compressed { + padding-top: var(--spacing); + padding-bottom: var(--spacing); +} + +.Listbox-item--standard { + padding-top: var(--spacing-l); + padding-bottom: var(--spacing-l); +} + +/* Listbox type - option */ + +.Listbox-item--option { + cursor: pointer; +} + +.Listbox-item--option:hover { + background-color: var(--secondary-lightest); +} + +.Listbox-item--option:focus, +.Listbox-item--option:focus-visible { + outline: 3px auto var(--secondary-shadow); +} + +.Listbox-item--option:active { + background: rgba(255, 245, 199, 0.6); +} + +.Listbox-item--selected { + background: rgba(255, 245, 199, 0.6); +} + +/* Listbox type - resource */ + +.Listbox-item--resource { + cursor: pointer; +} + +.Listbox-item--resource:hover { + background-color: var(--secondary-lightest); +} + +.Listbox-item--resource:focus, +.Listbox-item--resource:focus-visible { + outline: 3px auto var(--secondary-shadow); +} + +.Listbox-item--resource:active { + background: rgba(255, 245, 199, 0.6); +} + +.Listbox-item--activated { + background: rgba(255, 245, 199, 0.6); +} + +/* Listbox type - description */ + +.Listbox-item--description:focus, +.Listbox-item--description:focus-visible { + outline: none; +} + +/* Listbox type - disabled */ + +.Listbox-item--disabled { + opacity: 0.4; + pointer-events: none; +} + +/* Listbox type - draggable */ + +.Listbox-item--draggable:focus { + outline: 3px auto var(--secondary-shadow); +} + +.Listbox-item--drag-icon { + cursor: grab; + margin-right: var(--spacing); +} + +.Listbox-item--drag-icon:hover { + color: var(--inverse); +} + +.Listbox-item--drag-icon:active { + color: var(--primary); +} diff --git a/css/src/core/animation.css b/css/src/core/animation.css index 6f51889108..a169c2ef82 100644 --- a/css/src/core/animation.css +++ b/css/src/core/animation.css @@ -97,3 +97,14 @@ animation: fadeIn var(--duration--moderate-01) var(--entrance-expressive-curve), entryRightCurve var(--duration--moderate-02) var(--entrance-expressive-curve); } + +.rotate-clockwise { + transform: rotateZ(360deg); + transition: var(--duration--moderate-02) var(--standard-productive-curve); + animation-fill-mode: forwards; +} + +.rotate-anticlockwise { + transform: rotateZ(180deg); + transition: var(--duration--moderate-02) var(--standard-productive-curve); +} diff --git a/docs/src/data/components/images/Listbox.png b/docs/src/data/components/images/Listbox.png new file mode 100644 index 0000000000..aefc15024a Binary files /dev/null and b/docs/src/data/components/images/Listbox.png differ diff --git a/docs/src/data/components/index.js b/docs/src/data/components/index.js index ed0a6e21d5..6bec76ea07 100644 --- a/docs/src/data/components/index.js +++ b/docs/src/data/components/index.js @@ -135,6 +135,13 @@ export const data = [ code: 'Available', image: () => , }, + { + link: 'listbox/usage', + name: 'Listbox', + design: 'Available', + code: 'Available', + image: () => , + }, { link: 'message/usage', name: 'Messages', diff --git a/docs/src/data/nav/components.yaml b/docs/src/data/nav/components.yaml index 18f0e1ff32..ac4f664b88 100644 --- a/docs/src/data/nav/components.yaml +++ b/docs/src/data/nav/components.yaml @@ -84,6 +84,10 @@ menus: link: /components/linkButton/usage/ hideInMobile: true + - label: Listbox + link: /components/listbox/usage/ + hideInMobile: true + - label: List link: /components/list/usage/ hideInWeb: true diff --git a/docs/src/pages/components/listbox/images/Collapsing-item.gif b/docs/src/pages/components/listbox/images/Collapsing-item.gif new file mode 100644 index 0000000000..8dc7b92162 Binary files /dev/null and b/docs/src/pages/components/listbox/images/Collapsing-item.gif differ diff --git a/docs/src/pages/components/listbox/images/Compressed.png b/docs/src/pages/components/listbox/images/Compressed.png new file mode 100644 index 0000000000..382272add9 Binary files /dev/null and b/docs/src/pages/components/listbox/images/Compressed.png differ diff --git a/docs/src/pages/components/listbox/images/Description-list.png b/docs/src/pages/components/listbox/images/Description-list.png new file mode 100644 index 0000000000..0507f585c7 Binary files /dev/null and b/docs/src/pages/components/listbox/images/Description-list.png differ diff --git a/docs/src/pages/components/listbox/images/Expanding-item.gif b/docs/src/pages/components/listbox/images/Expanding-item.gif new file mode 100644 index 0000000000..aab6d95941 Binary files /dev/null and b/docs/src/pages/components/listbox/images/Expanding-item.gif differ diff --git a/docs/src/pages/components/listbox/images/Nested-list.png b/docs/src/pages/components/listbox/images/Nested-list.png new file mode 100644 index 0000000000..05c34cfa91 Binary files /dev/null and b/docs/src/pages/components/listbox/images/Nested-list.png differ diff --git a/docs/src/pages/components/listbox/images/Option-list.png b/docs/src/pages/components/listbox/images/Option-list.png new file mode 100644 index 0000000000..be6445290d Binary files /dev/null and b/docs/src/pages/components/listbox/images/Option-list.png differ diff --git a/docs/src/pages/components/listbox/images/Reordering-items.gif b/docs/src/pages/components/listbox/images/Reordering-items.gif new file mode 100644 index 0000000000..7bffca0194 Binary files /dev/null and b/docs/src/pages/components/listbox/images/Reordering-items.gif differ diff --git a/docs/src/pages/components/listbox/images/Reordering.png b/docs/src/pages/components/listbox/images/Reordering.png new file mode 100644 index 0000000000..050cbecebc Binary files /dev/null and b/docs/src/pages/components/listbox/images/Reordering.png differ diff --git a/docs/src/pages/components/listbox/images/Resource-list.png b/docs/src/pages/components/listbox/images/Resource-list.png new file mode 100644 index 0000000000..6402730e08 Binary files /dev/null and b/docs/src/pages/components/listbox/images/Resource-list.png differ diff --git a/docs/src/pages/components/listbox/images/Standard.png b/docs/src/pages/components/listbox/images/Standard.png new file mode 100644 index 0000000000..6402730e08 Binary files /dev/null and b/docs/src/pages/components/listbox/images/Standard.png differ diff --git a/docs/src/pages/components/listbox/images/States-Description-list.png b/docs/src/pages/components/listbox/images/States-Description-list.png new file mode 100644 index 0000000000..ba34982db4 Binary files /dev/null and b/docs/src/pages/components/listbox/images/States-Description-list.png differ diff --git a/docs/src/pages/components/listbox/images/States-Drag-Indicator-Icon.png b/docs/src/pages/components/listbox/images/States-Drag-Indicator-Icon.png new file mode 100644 index 0000000000..d3e1cbcf5e Binary files /dev/null and b/docs/src/pages/components/listbox/images/States-Drag-Indicator-Icon.png differ diff --git a/docs/src/pages/components/listbox/images/States-Option-list.png b/docs/src/pages/components/listbox/images/States-Option-list.png new file mode 100644 index 0000000000..72f57bc9c8 Binary files /dev/null and b/docs/src/pages/components/listbox/images/States-Option-list.png differ diff --git a/docs/src/pages/components/listbox/images/States-Resource-list.png b/docs/src/pages/components/listbox/images/States-Resource-list.png new file mode 100644 index 0000000000..8f7b4ebda4 Binary files /dev/null and b/docs/src/pages/components/listbox/images/States-Resource-list.png differ diff --git a/docs/src/pages/components/listbox/images/Structure.png b/docs/src/pages/components/listbox/images/Structure.png new file mode 100644 index 0000000000..4c8bdd67b9 Binary files /dev/null and b/docs/src/pages/components/listbox/images/Structure.png differ diff --git a/docs/src/pages/components/listbox/images/Tight.png b/docs/src/pages/components/listbox/images/Tight.png new file mode 100644 index 0000000000..ee3614ad78 Binary files /dev/null and b/docs/src/pages/components/listbox/images/Tight.png differ diff --git a/docs/src/pages/components/listbox/images/Vertical-arrangement.png b/docs/src/pages/components/listbox/images/Vertical-arrangement.png new file mode 100644 index 0000000000..7539f950c3 Binary files /dev/null and b/docs/src/pages/components/listbox/images/Vertical-arrangement.png differ diff --git a/docs/src/pages/components/listbox/interactions.mdx b/docs/src/pages/components/listbox/interactions.mdx new file mode 100644 index 0000000000..b607eaefca --- /dev/null +++ b/docs/src/pages/components/listbox/interactions.mdx @@ -0,0 +1,258 @@ +### Reordering list items + +To reorder, pick the item to be reordered via the drag handle placed at the beginning. Items other than the picked item will reorder as soon as the picked item reaches the boundary of the adjacent item. + +**Note:** To make the reordering of list items accessible, there is a set of keyboard shortcuts: +- tab and shift+tab to focus a item +- space to lift or drop the item +- arrow down to move the lifted item down +- arrow up to move the lifted item up +- escape to cancel the lift and return the item to its initial position + +
    + +Reordering a list item using the drag indicator + +Reordering a list item using the drag indicator + +#### Picked item + +
    +
    + + + + + + + + + + + + + + + + + + + +
    PropertyInitialFinal
    FillNil#FFFFFF
    ShadowNilShadow-30
    + +
    + +#### Other items + +
    +
    + + + + + + + + + + + + + + +
    PropertyInitialFinal
    Y positionCurrent positionReordered position
    + +
    + +**Curve** + + + + + + + + + + + + +
    CurveDuration
    cubic-bezier(0.2, 0, 0.38, 0.9)240ms
    + +
    + +### Nested list + +#### Expanding an item + +
    +
    + +Expanding an item in a nested list + +Expanding an item in a nested list + +##### Expand icon + +
    +
    + + + + + + + + + + + + + + +
    PropertyInitialFinal
    Rotation180° (clockwise)
    + +
    + +**Curve** + + + + + + + + + + + + +
    CurveDuration
    cubic-bezier(0.2, 0, 0.38, 0.9)240ms
    + +
    + +##### Nested content + +
    +
    + + + + + + + + + + + + + + +
    PropertyInitialFinal
    Height0pxheight
    + +
    + +**Curve** + + + + + + + + + + + + +
    CurveDuration
    cubic-bezier(0, 0, 0.38, 0.9)240ms
    + +
    + +#### Collapsing an item + +
    +
    + +Collapsing an item in a nested list + +Collapsing an item in a nested list + +##### Collapse icon + +
    +
    + + + + + + + + + + + + + + +
    PropertyInitialFinal
    Rotation180°0° (anti-clockwise)
    + +
    + +**Curve** + + + + + + + + + + + + +
    CurveDuration
    cubic-bezier(0.2, 0, 0.38, 0.9)240ms
    + +
    + +##### Nested content + +
    +
    + + + + + + + + + + + + + + +
    PropertyInitialFinal
    Heightheight0px
    + +
    + +**Curve** + + + + + + + + + + + + +
    CurveDuration
    cubic-bezier(0.2, 0, 1, 0.9)160ms
    + +
    diff --git a/docs/src/pages/components/listbox/usage.mdx b/docs/src/pages/components/listbox/usage.mdx new file mode 100644 index 0000000000..15149b565c --- /dev/null +++ b/docs/src/pages/components/listbox/usage.mdx @@ -0,0 +1,214 @@ +--- +title: Listbox +description: Related content grouped together and arranged vertically or horizontally. +tabs: ['Usage', 'Interactions'] +showMobile: true +keywords: ['Grid', 'List'] +--- + +A listbox consists of related content grouped together and arranged vertically or horizontally. Listbox present the content (text, supporting visuals, etc) in a consistent layout to make them easily scannable. + +### Types +Listbox come in **3 types**: option list, description list and resource list + +#### Option list +A list of options where an option is an entity that a user can select/pick. + +![An option list](./images/Option-list.png) +An option list + +#### Description list +A list of items containing simple information which is meant for consumption only. It can occasionally contain minor actions such as copy, edit, remove, etc. + +![A description list](./images/Description-list.png) +A description list + +#### Resource list +A list of resources where a resource is an object in itself and has a detailed view linked to it. + +![A resource list](./images/Resource-list.png) +A resource list + +### Sizes +Listbox comes in **3 sizes**: standard, tight and compressed. The width varies based on the content and layout. + + + + + + + + + + + + + + + + + + + + + + + + +
    TypeVertical paddingHorizontal padding
    Standard12px16px
    Compressed8px16px
    Tight4px16px
    + +
    + +#### Standard + +
    +
    + +![Standard size - vertical padding - 12 px](./images/Standard.png) +Standard size - vertical padding - 12 px + +#### Compressed + +
    +
    + +![Compressed size - vertical padding - 8 px](./images/Compressed.png) +Compressed size - vertical padding - 8 px + +#### Tight + +This size is typically suited for information-dense lists. + +![Tight size - vertical padding - 4 px](./images/Tight.png) +Tight size - vertical padding - 4 px + +### States + +**Note**: Since description list item is not interactive, it does not have any state. + +#### Option list + +
    +
    + +![States of option list items](./images/States-Option-list.png) +States of option list items + +#### Description list + +
    +
    + +![States of description list items](./images/States-Description-list.png) +States of description list items + +#### Resource list + +
    +
    + +![States of resource list items](./images/States-Resource-list.png) +States of resource list items + +
    + +### Structure + +
    +
    + +![Structure of list items](./images/Structure.png) +Structure of list items + +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    PropertyValue(s)
    Horizontal padding16 px
    Vertical padding +
      +
    • Standard - 12 px
    • +
    • Compressed - 8 px
    • +
    • Tight - 4 px
    • +
    +
    Shadow of picked item (while reordering)Shadow 30
    BackgroundNil
    + +
    + +### Configurations + +
    +
    + + + + + + + + + + + + + + +
    PropertyValue(s)Default value
    Spacing between 2 items<value>0 px
    + +
    + +### Usage + +#### Arrangement + +##### Vertical + +This is the most common arrangement as the list is easy to read when arranged vertically. + +
    + +![List items arranged vertically](./images/Vertical-arrangement.png) +List items arranged vertically + +#### Nesting + +List can be nested to show additional content. Any type of content such as a list, a card, etc can be nested inside a list items. + +
    + +![A nested list](./images/Nested-list.png) +A nested list + +#### Reordering list items + +Reordering the list items should be hinted using a drag indicator placed at the beginning of the items. The list item can be picked and dragged using the drag indicator to reorder the list. + +
    + +![Reordering the list items](./images/Reordering.png) +Reordering the list items + +
    + +![States of drag indicator icon](./images/States-Drag-Indicator-Icon.png) +States of drag indicator icon