diff --git a/package.json b/package.json index 06d57f61b..69945809e 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,6 @@ }, "dependencies": { "@tippyjs/react": "^4.2.5", - "@types/react-window": "^1.8.4", "dayjs": "^1.8.34", "emotion-reset": "^3.0.1", "lodash": "^4.17.19", @@ -121,7 +120,6 @@ "react-media": "^2.0.0-rc.1", "react-range": "^1.8.12", "react-switch": "^6.0.0", - "react-window": "^1.8.6", "recharts": "^1.8.5", "tslib": "^2.4.1", "uuid": "^8.3.2" diff --git a/src/components/DropdownButton/DropdownButton.test.tsx b/src/components/DropdownButton/DropdownButton.test.tsx index b4037c2ae..1607a5ac5 100644 --- a/src/components/DropdownButton/DropdownButton.test.tsx +++ b/src/components/DropdownButton/DropdownButton.test.tsx @@ -48,7 +48,7 @@ describe('DropdownButton:', () => { userEvent.click(iconButton); - const option = screen.getByTestId('dropdown-button-options'); + const option = screen.getByTestId('ictinus_list_item_Item_1'); userEvent.click(option); diff --git a/src/components/DropdownButton/DropdownButton.tsx b/src/components/DropdownButton/DropdownButton.tsx index b1793f455..893ff7ef2 100644 --- a/src/components/DropdownButton/DropdownButton.tsx +++ b/src/components/DropdownButton/DropdownButton.tsx @@ -1,5 +1,6 @@ import { ClickEvent } from 'hooks/useLoading'; import useTheme from 'hooks/useTheme'; +import { head } from 'lodash'; import React, { useCallback } from 'react'; import { TestProps } from 'utils/types'; @@ -13,7 +14,7 @@ import { generateTestDataId } from '../../utils/helpers'; import Button from 'components/Button'; import { PrimitiveButtonTypes } from 'components/Button/Button.types'; import IconButton from 'components/IconButton'; -import List, { ListItemType } from 'components/List'; +import List, { ListItem, ListItemText, ListItemType, ListSelection } from 'components/List'; import ClickAwayListener from 'components/utils/ClickAwayListener'; import { MenuPositionAllowed, optionsStyle } from 'components/utils/DropdownOptions'; @@ -57,9 +58,9 @@ const DropdownButton = React.forwardRef( /** The CTA for the Options inside the Dropdown */ const handleOptionClick = useCallback( - (option: ListItemType) => { + (option: string | number) => { setIsOpen(false); - onOptionSelect(option.value); + onOptionSelect(option); }, [onOptionSelect] ); @@ -67,6 +68,16 @@ const DropdownButton = React.forwardRef( /** The CTA for the IconButton and the ClickAwayListener */ const handleIconButtonClick = useCallback(() => setIsOpen(!isOpen), [isOpen]); + const onSelectionChange = useCallback( + (keys: ListSelection) => { + setIsOpen(false); + const keyFound = String(head(Array.from(keys))); + const optionFound = items?.find((o) => o === keyFound); + optionFound && handleOptionClick(optionFound); + }, + [handleOptionClick, items] + ); + return ( setIsOpen(false)}>
@@ -106,11 +117,16 @@ const DropdownButton = React.forwardRef(
{items && ( ({ value: item, label: item }))} - rowSize={'small'} - handleOptionClick={handleOptionClick} + label={'dropdown-button'} + onSelectionChange={onSelectionChange} dataTestId={generateTestDataId('dropdown-button-options', dataTestPrefixId)} - /> + > + {items.map((item) => ( + + {item} + + ))} + )}
)} diff --git a/src/components/Filter/Filter.test.tsx b/src/components/Filter/Filter.test.tsx index 182b8bcc1..84ff84cdb 100644 --- a/src/components/Filter/Filter.test.tsx +++ b/src/components/Filter/Filter.test.tsx @@ -4,6 +4,7 @@ import { render, screen, waitFor } from 'test'; import { selectDropdownOption } from '../../test'; import Filter from './Filter'; +import { SELECT_ALL_OPTION } from '../Select/constants'; global.ResizeObserver = jest.fn().mockImplementation(() => ({ observe: jest.fn(), @@ -80,11 +81,13 @@ describe('Generic Filter', () => { const selectInput = screen.getByTestId('filter-input'); - expect(screen.getByTestId('ictinus_list_default_option')).toBeInTheDocument(); + expect(screen.getByTestId(`ictinus_list_item_${defaultFilter.value}`)).toBeInTheDocument(); userEvent.type(selectInput, 'test'); - expect(screen.queryByTestId('ictinus_list_default_option')).not.toBeInTheDocument(); + expect( + screen.queryByTestId(`ictinus_list_item_${defaultFilter.value}`) + ).not.toBeInTheDocument(); }); it('should display loading dots when isLoading is true', async () => { @@ -214,7 +217,7 @@ describe('Multi Filter', () => { }); it('selects all options and changes label when Select All is clicked', async () => { - userEvent.click(screen.getByTestId('ictinus_list_default_option')); + userEvent.click(screen.getByTestId(`ictinus_list_item_${SELECT_ALL_OPTION.value}`)); userEvent.click(button); diff --git a/src/components/Filter/components/Options/Options.tsx b/src/components/Filter/components/Options/Options.tsx index 49a1b7e16..4338ae55b 100644 --- a/src/components/Filter/components/Options/Options.tsx +++ b/src/components/Filter/components/Options/Options.tsx @@ -1,9 +1,11 @@ -import React from 'react'; +import { flatMap, head } from 'lodash'; +import React, { useCallback } from 'react'; import { emptyStyle } from './Options.style'; +import { SELECT_ALL_OPTION } from '../../../Select/constants'; import { FilterOption } from '../../types'; import { FILTER_OPTIONS_MAX_HEIGHT } from 'components/Filter/utils'; -import List from 'components/List'; +import List, { ListItem, ListItemText, ListSelection } from 'components/List'; import { MAX_NON_VIRTUALIZED_ITEMS_FILTER } from 'components/List/utils'; export interface Props { @@ -30,19 +32,44 @@ const Options: React.FC = ({ const height = isForcedVirtualized ? FILTER_OPTIONS_MAX_HEIGHT : undefined; const defaultOption = isDefaultOptionVisible ? defaultValue : undefined; + const onSelectionChange = useCallback( + (keys: ListSelection) => { + const keyFound = String(head(Array.from(keys))); + if (keyFound === SELECT_ALL_OPTION.value) { + onSelect(SELECT_ALL_OPTION); + } else { + const optionFound = flatMap(items, (o) => o.options || o).find( + (o) => String(o.value) === keyFound + ); + optionFound && onSelect(optionFound); + } + }, + [items, onSelect] + ); return items.length ? ( option.value !== defaultValue.value)} - rowSize={'small'} - defaultOption={defaultOption} - selectedItem={selectedItem} - isSearchable={isSearchable} - handleOptionClick={(option: FilterOption) => onSelect(option)} + label={'filter-options'} + selectedKeys={selectedItem ? [selectedItem.value] : []} + disabledKeys={items.filter((o) => o.isDisabled).map((o) => o.value)} + onSelectionChange={onSelectionChange} isVirtualized={isVirtualized && isForcedVirtualized} height={height} dataTestId={dataTestId} - /> + > + {defaultOption && ( + + {defaultOption.label} + + )} + {items + .filter((option) => option.value !== defaultValue.value) + .map((item) => ( + + {item.label} + + ))} + ) : (
No options
); diff --git a/src/components/List/List.stories.mdx b/src/components/List/List.stories.mdx deleted file mode 100644 index 6626726a8..000000000 --- a/src/components/List/List.stories.mdx +++ /dev/null @@ -1,111 +0,0 @@ -import { Meta, Story, Preview, Props } from '@storybook/addon-docs'; -import { select, number, boolean } from '@storybook/addon-knobs'; -import ListShowcase from '../storyUtils/ListShowcase'; -import List from './List'; -import { FIGMA_URL } from '../../utils/common'; - - - -# List - -A universal List component with different types. - -## Usage - -```js -import { List } from '@orfium/ictinus'; - -; -``` - -## Props - - - -### List - -List component. - - - - - - - -### Virtualized List with knobs - -Virtualized List component for large amounts of items. - - - - - - - -### List with icons - -Virtualized List component for large amounts of items. - - - - - - diff --git a/src/components/List/List.style.ts b/src/components/List/List.style.ts index 749ca8ff0..1a90afdd1 100644 --- a/src/components/List/List.style.ts +++ b/src/components/List/List.style.ts @@ -11,16 +11,6 @@ export const wrapperStyle = width: ${`${width}px` || '100%'}; `; -export const listLabelWithHelper: SerializedStyles = css` - display: flex; - flex-direction: column; - cursor: inherit; -`; - -export const listLabel: SerializedStyles = css` - cursor: inherit; -`; - export const listStyle = ({ width, height, isSearchable }: { width?: number; height?: number; isSearchable?: boolean }) => (theme: Theme): SerializedStyles => @@ -30,14 +20,15 @@ export const listStyle = margin-bottom: 0; border-radius: ${isSearchable ? 'initial' : theme.globals.spacing.get('3')}; width: ${width ? rem(width) : '100%'}; - height: ${height ? rem(height) : '100%'}; + height: ${height ? rem(height) : 'auto'}; overflow: auto; overflow-x: hidden; + background: ${theme.globals.colors.white}; `; -export const listLabelHelperText = (theme: Theme): SerializedStyles => css` - font-size: ${theme.globals.typography.fontSize.get('1')}; - font-weight: ${theme.globals.typography.fontWeight.get('regular')}; - color: ${theme.utils.getColor('lightGrey', 650)}; - cursor: inherit; +export const groupedUlStyle = (): SerializedStyles => css` + { + padding: 0; + list-style: none; + } `; diff --git a/src/components/List/List.tsx b/src/components/List/List.tsx index 62c250465..6103b0142 100644 --- a/src/components/List/List.tsx +++ b/src/components/List/List.tsx @@ -1,90 +1,169 @@ -import React, { memo } from 'react'; -import isEqual from 'react-fast-compare'; +import { useFocusRing } from '@react-aria/focus'; +import { useListBox, useOption } from '@react-aria/listbox'; +import { mergeProps } from '@react-aria/utils'; +import { useListState } from '@react-stately/list'; +import React from 'react'; +import { AriaListBoxProps, useListBoxSection } from 'react-aria'; import { TestProps } from 'utils/types'; -import { wrapperStyle } from './List.style'; -import NormalList from './NormalList'; -import { ListItemType, ListRowSize, SelectHandlerType } from './types'; -import VirtualizedList from './VirtualizedList'; +import ListItemWrapper from './components/ListItemWrapper/ListItemWrapper'; +import { ListItemWrapperStyled } from './components/ListItemWrapper/ListItemWrapper.style'; +import { groupedUlStyle, listStyle, wrapperStyle } from './List.style'; +import { ListSelected, ListSelection } from './types'; +import { COMPACT_LIST_ITEM_HEIGHT, NORMAL_LIST_ITEM_HEIGHT } from './utils'; +import Window from './Window'; +import useCombinedRefs from '../../hooks/useCombinedRefs'; +import { SelectOption } from '../Select'; export type ListProps = { - /** Data for the list */ - data: ListItemType[]; - /** Size of the list's row (height of ListItem Component) */ - rowSize: ListRowSize; + /** The label that describes the List, useful to determine aria and accessibility of the list */ + label: string; /** Width of the list */ width?: number; - /** Height of the list */ + /** Height of the list when you use it as virtualized */ height?: number; /** Virtualized list option */ isVirtualized?: boolean; - /** Selected Item */ - selectedItem?: ListItemType; - /** Default option. Renders on top of the list, highlighted - for Filter component */ - defaultOption?: ListItemType; - /** Search Term to be highlighted in list items */ - searchTerm?: string; - /** Option Click handler for SelectOption[] data case */ - handleOptionClick?: SelectHandlerType; - /** Defines if this is searchable list or not **/ - isSearchable?: boolean; -} & TestProps & - React.InputHTMLAttributes; + /** Callback when an item gets a change */ + onSelectionChange?: (keys: ListSelection) => unknown; + /** Is the actual `key` of the item e.g `` is the `item_1` */ + disabledKeys?: ListSelected; + /** Is the actual `key` of the item e.g `` is the `item_1` */ + selectedKeys?: ListSelected; + // @TODO fix this on Select/Filter part as this only affect those + // /** Search Term to be highlighted in list items */ + // searchTerm?: string; + // /** Defines if this is searchable list or not **/ + // isSearchable?: boolean; +} & Omit, 'selectionMode' | 'onSelectionChange' | 'children'> & + TestProps & + Omit, 'onChange'>; -const List = React.forwardRef( - ( - { - data, - rowSize, - width, - height, - isVirtualized = false, - selectedItem, - isSearchable, - defaultOption, - searchTerm, - handleOptionClick, - dataTestId, - ...rest - }, - ref - ) => { - return ( -
- {isVirtualized ? ( - - ) : ( - - )} +/** + * This is the List component that uses the Window underneath for every UL. + * Because this component uses the React-Aria abstraction for the List it will also contain the Option and ListBoxSection below + * as a masked layer to the actual subcomponents + * */ +const List = React.forwardRef((props, ref) => { + const { + width, + height, + isVirtualized = false, + // isSearchable, + // searchTerm, + dataTestId, + } = props; + /** ignore the change on the `onSelectionChange` from any to unknown **/ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const state = useListState({ ...props, selectionMode: 'single' }); + const myRef = React.useRef(null); + const combinedRefs = useCombinedRefs(myRef, ref); + /** ignore the change on the `onSelectionChange` from any to unknown **/ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const { listBoxProps } = useListBox(props, state, combinedRefs); + + const firstKey = state.collection.getFirstKey(); + const first = firstKey ? state.collection.getItem(firstKey) : null; + + return ( +
+
+
+ + {Array.from(state.collection).map((item) => { + return item.type === 'section' ? ( + + ) : ( + +
- ); - } -); +
+ ); +}); List.displayName = 'List'; -export default memo(List, isEqual); +/** + * The Option overlay component for React-Aria. The actual subcomponent rendered is the `ListItemWrapper` + */ +function Option({ item, state, style }: { item: any; state: any; style?: any }) { + // Get props for the option element + const ref = React.useRef(null); + const { optionProps, isDisabled } = useOption({ key: item.key }, state, ref); + + // Determine whether we should show a keyboard + // focus ring for accessibility + const { isFocusVisible, isFocused, focusProps } = useFocusRing(); + + return ( + + {item.rendered} + + ); +} + +/** + * The Group overlay component for React-Aria. The actual subcomponent rendered is the `Option` which will render + * `ListItemWrapper` at the end. + */ +function ListBoxSection({ section, state }: any) { + const { itemProps, headingProps, groupProps } = useListBoxSection({ + heading: section.rendered, + 'aria-label': section['aria-label'], + }); + + return ( + <> + + {section.rendered && ( + + {section.rendered} + + )} +
    + {[...section.childNodes].map((node) => ( +
+
+ + ); +} + +export default List; diff --git a/src/components/List/ListItem.tsx b/src/components/List/ListItem.tsx new file mode 100644 index 000000000..06b934d27 --- /dev/null +++ b/src/components/List/ListItem.tsx @@ -0,0 +1,23 @@ +import { Item as AriaItem } from '@react-stately/collections'; +import React from 'react'; + +import { ListRowSize } from './types'; + +export type ListItemProps = { + /** A string representation of the item's unique key. */ + key?: string | number; + /** A string representation of the item's contents, used when contents are something more than just text to determine labels etc. */ + textValue?: string; + /** @default normal */ + rowSize?: ListRowSize; +}; +const ListItem: React.FC = (props) => ( + + {props.children} + +); + +// @ts-ignore hack to pass the aria generator to the component as needed +ListItem.getCollectionNode = AriaItem.getCollectionNode; + +export default ListItem; diff --git a/src/components/List/ListItem/ListItem.style.ts b/src/components/List/ListItem/ListItem.style.ts deleted file mode 100644 index 125b8512d..000000000 --- a/src/components/List/ListItem/ListItem.style.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { css, SerializedStyles } from '@emotion/react'; -import { Theme } from 'theme'; -import { rem } from 'theme/utils'; - -import { ListRowSize } from '../types'; - -export const listItemStyle = - ({ - size, - isHighlighted, - isDisabled, - isGroupItem, - }: { - size: ListRowSize; - isHighlighted: boolean; - isDisabled: boolean; - isGroupItem?: boolean; - }) => - (theme: Theme): SerializedStyles => - css` - height: ${size === 'normal' ? rem(56) : rem(46)}; - font-size: ${theme.globals.typography.fontSize.get(size === 'normal' ? '4' : '3')}; - background-color: ${theme.globals.colors.white}; - display: flex; - align-items: center; - padding: 0px ${theme.globals.spacing.get('6')} 0px - ${isGroupItem ? theme.globals.spacing.get('9') : theme.globals.spacing.get('6')}; - cursor: pointer; - - ${isHighlighted && 'font-weight: 500;'} - - &[data-focus-visible] { - background-color: ${theme.utils.getColor('lightGrey', 50)}; - } - - &[aria-selected='true'] { - background-color: ${theme.utils.getColor('blue', 50)}; - color: ${theme.utils.getColor('blue', 550)}; - font-weight: ${theme.globals.typography.fontWeight.get('medium')}; - - &[data-focus-visible] { - background-color: ${theme.utils.getColor('lightGrey', 50)}; - } - } - - &[aria-disabled] { - color: var(--text-color-disabled); - } - - strong { - font-weight: bold; - } - - &:hover { - background-color: ${theme.utils.getColor('lightGrey', 50)}; - } - - ${isDisabled && - ` - opacity: 0.5; - cursor: not-allowed; - `} - `; - -export const contentStyle = (): SerializedStyles => css` - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - cursor: inherit; - flex: 1; - flex-direction: row; - display: flex; - > div { - flex: 1; - } -`; diff --git a/src/components/List/ListItem/ListItem.tsx b/src/components/List/ListItem/ListItem.tsx deleted file mode 100644 index 0ee23a225..000000000 --- a/src/components/List/ListItem/ListItem.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; -import { Item } from 'react-aria-components'; -import { TestProps } from 'utils/types'; - -import { listItemStyle, contentStyle } from './ListItem.style'; -import { ListItemType, ListRowSize, SelectHandlerType } from '../types'; -import { renderContent } from '../utils'; - -export type ListItemProps = { - /** Size of the ListItem (translates to height) */ - size: ListRowSize; - /** Content of the ListItem */ - content: ListItemType; - /** Index, for test-id calculation */ - index: number | string; - /** Selected state */ - isSelected?: boolean; - /** Whether the text of the ListItem is highlighted or not. eg: Filter - Default Value */ - isHighlighted?: boolean; - /** Disabled state */ - isDisabled?: boolean; - /** Search Term to be highlighted in list items */ - searchTerm?: string; - /** Option Click handler for SelectOption[] data case */ - handleOptionClick?: SelectHandlerType; - /** Determines the left padding */ - isGroupItem?: boolean; -} & TestProps & - Omit, 'value'>; - -const ListItem = React.forwardRef( - ( - { - size, - content, - index, - isSelected = false, - isHighlighted = false, - isDisabled = false, - handleOptionClick, - searchTerm, - dataTestId, - isGroupItem, - ...rest - }, - ref - ) => { - return ( - -
- { - /** @TODO latest version typescript 4.4 is solving this as a constant */ - renderContent(content, searchTerm) - } -
-
- ); - } -); -ListItem.displayName = 'ListItem'; - -export default ListItem; diff --git a/src/components/List/ListItem/index.ts b/src/components/List/ListItem/index.ts deleted file mode 100644 index d0ef0af7f..000000000 --- a/src/components/List/ListItem/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './ListItem'; -export * from './ListItem'; diff --git a/src/components/List/ListItemGroup/ListGroupTitle/ListGroupTitle.style.ts b/src/components/List/ListItemGroup/ListGroupTitle/ListGroupTitle.style.ts deleted file mode 100644 index 5cedb65b9..000000000 --- a/src/components/List/ListItemGroup/ListGroupTitle/ListGroupTitle.style.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { css, SerializedStyles } from '@emotion/react'; -import { Theme } from 'theme'; -import { rem } from 'theme/utils'; - -import { ListRowSize } from '../../types'; - -export const listGroupTitleStyle = - ({ size, isDisabled }: { size: ListRowSize; isDisabled?: boolean }) => - (theme: Theme): SerializedStyles => - css` - height: ${size === 'normal' ? rem(56) : rem(46)}; - font-size: ${theme.globals.typography.fontSize[size === 'normal' ? '13' : '11']}; - background-color: ${theme.globals.colors.white}; - color: ${theme.utils.getColor('lightGrey', 650)}; - display: flex; - align-items: center; - padding: 0px ${theme.globals.spacing.get('6')}; - font-weight: ${theme.globals.typography.fontWeight.get('bold')}; - - ${isDisabled && - ` - opacity: 0.5; - cursor: not-allowed; - `} - `; - -export const contentStyle = (): SerializedStyles => css` - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -`; diff --git a/src/components/List/ListItemGroup/ListGroupTitle/ListGroupTitle.tsx b/src/components/List/ListItemGroup/ListGroupTitle/ListGroupTitle.tsx deleted file mode 100644 index dc3526f08..000000000 --- a/src/components/List/ListItemGroup/ListGroupTitle/ListGroupTitle.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import { TestProps } from 'utils/types'; - -import { contentStyle, listGroupTitleStyle } from './ListGroupTitle.style'; -import { ListItemType, ListRowSize } from 'components/List/types'; -import { renderContent } from 'components/List/utils'; -import { SelectOption } from 'components/Select'; - -export type ListGroupTitleProps = { - /** Size of the ListGroupTitle (translates to height) */ - size: ListRowSize; - /** Content of the ListItem */ - content: ListItemType; - /** Index, for test-id calculation */ - index: number; - /** Search Term to be highlighted in list items */ - searchTerm?: string; -} & TestProps; - -const ListGroupTitle: React.FC = ({ - size, - content, - index, - searchTerm, - dataTestId, -}) => { - return ( -
-
{renderContent(content, searchTerm)}
-
- ); -}; -ListGroupTitle.displayName = 'ListGroupTitle'; - -export default ListGroupTitle; diff --git a/src/components/List/ListItemGroup/ListGroupTitle/index.ts b/src/components/List/ListItemGroup/ListGroupTitle/index.ts deleted file mode 100644 index d37b9590c..000000000 --- a/src/components/List/ListItemGroup/ListGroupTitle/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './ListGroupTitle'; -export * from './ListGroupTitle'; diff --git a/src/components/List/ListItemGroup/ListItemGroup.tsx b/src/components/List/ListItemGroup/ListItemGroup.tsx deleted file mode 100644 index 9e0e48c79..000000000 --- a/src/components/List/ListItemGroup/ListItemGroup.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; -import { generateUniqueKey } from 'utils/helpers'; -import { TestProps } from 'utils/types'; - -import ListGroupTitle from './ListGroupTitle'; -import { listStyle } from '../List.style'; -import ListItem from '../ListItem'; -import { ListItemType, ListRowSize, SelectHandlerType } from '../types'; -import { isSelected } from '../utils'; -import { SelectOption } from 'components/Select'; - -export type ListItemGroupProps = { - /** Size of the ListItem (translates to height) */ - size: ListRowSize; - /** Content of the ListItemGroup */ - content: ListItemType; - /** groupIndex, for test-id calculation */ - groupIndex: number; - /** Selected Item */ - selectedItem?: ListItemType; - /** Search Term to be highlighted in list items */ - searchTerm?: string; - /** Option Click handler for SelectOption[] data case */ - handleOptionClick?: SelectHandlerType; -} & TestProps; - -const ListItemGroup = React.forwardRef( - ({ size, content, groupIndex, selectedItem, searchTerm, handleOptionClick, dataTestId }, ref) => { - return ( -
  • - -
      - {(content as SelectOption).options?.map((option, index) => ( -
    • - -
    • - ))} -
    -
  • - ); - } -); -ListItemGroup.displayName = 'ListItemGroup'; - -export default ListItemGroup; diff --git a/src/components/List/ListItemGroup/index.ts b/src/components/List/ListItemGroup/index.ts deleted file mode 100644 index 856c40090..000000000 --- a/src/components/List/ListItemGroup/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './ListItemGroup'; -export * from './ListItemGroup'; diff --git a/src/components/List/ListSection.tsx b/src/components/List/ListSection.tsx new file mode 100644 index 000000000..fca6a47a6 --- /dev/null +++ b/src/components/List/ListSection.tsx @@ -0,0 +1,21 @@ +import { Section as AriaSection } from '@react-stately/collections'; +import { SectionProps } from '@react-types/shared'; +import React, { ReactNode } from 'react'; + +import { ListRowSize } from './types'; + +export type ListSectionProps = { + /** Rendered contents of the section, e.g. a header. */ + title: ReactNode; + /** An accessibility label for the section. */ + 'aria-label'?: string; + /** @default normal */ + rowSize?: ListRowSize; +} & SectionProps; + +const ListSection: React.FC = (props) => ; + +// @ts-ignore hack to pass the aria generator to the component as needed +ListSection.getCollectionNode = AriaSection.getCollectionNode; + +export default ListSection; diff --git a/src/components/List/NormalList/NormalList.style.ts b/src/components/List/NormalList/NormalList.style.ts deleted file mode 100644 index 8b1378917..000000000 --- a/src/components/List/NormalList/NormalList.style.ts +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/components/List/NormalList/NormalList.tsx b/src/components/List/NormalList/NormalList.tsx deleted file mode 100644 index ae4a9f4ca..000000000 --- a/src/components/List/NormalList/NormalList.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import React from 'react'; -import { ListBox } from 'react-aria-components'; -import { generateUniqueKey } from 'utils/helpers'; -import { TestProps } from 'utils/types'; - -import { SelectOption } from '../../Select'; -import { listStyle } from '../List.style'; -import ListItem from '../ListItem'; -import ListItemGroup from '../ListItemGroup'; -import { ListItemType, ListRowSize, SelectHandlerType } from '../types'; - -type NormalListProps = { - items: ListItemType[]; - /** Size of the list's row (height of ListItem Component) */ - rowSize: ListRowSize; - /** Width of the list */ - width?: number; - /** Height of the list */ - height?: number; - /** Selected Item */ - selectedItem?: ListItemType; - /** Default option. Renders on top of the list, highlighted */ - defaultOption?: ListItemType; - /** Search Term to be highlighted in list items */ - searchTerm?: string; - /** Option Click handler for SelectOption[] data case */ - handleOptionClick?: SelectHandlerType; - /** Defines if this is a searchable list or not **/ - isSearchable?: boolean; -} & TestProps; - -const NormalList = React.forwardRef( - ( - { - items, - width, - height, - rowSize, - selectedItem, - defaultOption, - isSearchable, - searchTerm, - handleOptionClick, - dataTestId, - ...rest - }, - ref - ) => { - const newItems = defaultOption ? [defaultOption, ...items] : items; - - const selectedOption = newItems.find((item) => item.value === selectedItem?.value); - - return ( -
    - } - css={listStyle({ width, height, isSearchable })} - selectionMode="single" - {...rest} - selectedKeys={ - selectedOption !== undefined && selectedOption !== null - ? new Set([selectedOption.value]) - : undefined - } - disabledKeys={ - new Set(items.filter((option) => option.isDisabled).map((option) => option.value)) - } - onSelectionChange={(keys) => { - const optionFound = newItems.find( - (item) => String(item.value) === String([...keys][0]) - ) as ListItemType; - optionFound && - handleOptionClick && - handleOptionClick( - optionFound.isCreated - ? { ...optionFound, label: String(optionFound.value) } - : optionFound - ); - }} - aria-label={dataTestId ? `${dataTestId}_list` : 'ictinus_list'} - > - {newItems.map((item, index) => - (item as SelectOption)?.options ? ( - - ) : ( - - ) - )} - -
    - ); - } -); -NormalList.displayName = 'NormalList'; - -export default NormalList; diff --git a/src/components/List/NormalList/index.ts b/src/components/List/NormalList/index.ts deleted file mode 100644 index a370c3eb6..000000000 --- a/src/components/List/NormalList/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './NormalList'; -export * from './NormalList'; diff --git a/src/components/List/VirtualizedList/VirtualizedList.style.ts b/src/components/List/VirtualizedList/VirtualizedList.style.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/components/List/VirtualizedList/VirtualizedList.tsx b/src/components/List/VirtualizedList/VirtualizedList.tsx deleted file mode 100644 index ee8a977bf..000000000 --- a/src/components/List/VirtualizedList/VirtualizedList.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import React, { useCallback, useMemo } from 'react'; -import { VariableSizeList as VList } from 'react-window'; -import { CSSProperties } from 'styled-components'; -import { TestProps } from 'utils/types'; - -import { SelectOption } from '../../Select'; -import { listStyle } from '../List.style'; -import ListItem from '../ListItem'; -import ListItemGroup from '../ListItemGroup'; -import { ListItemType, ListRowSize, SelectHandlerType } from '../types'; -import { isSelected, MAX_LARGE_HEIGHT, MAX_SMALL_HEIGHT } from '../utils'; - -type VirtualizedListProps = { - items: ListItemType[]; - /** Size of the list's row (height of ListItem Component) */ - rowSize: ListRowSize; - /** Width of the list */ - customWidth?: number; - /** Height of the list */ - customHeight?: number; - /** Selected Item */ - selectedItem?: ListItemType; - /** Default option. Renders on top of the list, highlighted */ - defaultOption?: ListItemType; - /** Search Term to be highlighted in list items */ - searchTerm?: string; - /** Option Click handler for SelectOption[] data case */ - handleOptionClick?: SelectHandlerType; -} & TestProps; - -const VirtualizedList = React.forwardRef( - ( - { - items, - customWidth, - customHeight, - rowSize, - selectedItem, - defaultOption, - searchTerm, - handleOptionClick, - dataTestId, - }, - ref - ) => { - const data = useMemo( - () => (defaultOption ? [defaultOption, ...items] : items), - [defaultOption, items] - ); - - const itemSize = useCallback( - (index: number) => { - const sizeBase = rowSize === 'normal' ? 56 : 46; - - if ((data[index] as SelectOption)?.options) { - return (((data[index] as SelectOption)?.options?.length as number) + 1) * sizeBase; - } - - return sizeBase; - }, - [data, rowSize] - ); - - const rowRenderer = ({ index, style }: { index: number; style: CSSProperties }) => { - return ( - - {(data[index] as SelectOption)?.options ? ( -
      - -
    - ) : ( - - )} -
    - ); - }; - - return ( - - {rowRenderer} - - ); - } -); -VirtualizedList.displayName = 'VirtualizedList'; - -export default VirtualizedList; diff --git a/src/components/List/VirtualizedList/index.ts b/src/components/List/VirtualizedList/index.ts deleted file mode 100644 index ca7be532b..000000000 --- a/src/components/List/VirtualizedList/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './VirtualizedList'; -export * from './VirtualizedList'; diff --git a/src/components/List/Window.tsx b/src/components/List/Window.tsx new file mode 100644 index 000000000..ad5dfcf32 --- /dev/null +++ b/src/components/List/Window.tsx @@ -0,0 +1,87 @@ +import useElementSize from 'hooks/useElementSize'; +import { throttle } from 'lodash'; +import React, { forwardRef } from 'react'; + +import useCombinedRefs from '../../hooks/useCombinedRefs'; + +export type WindowProps = { + rowHeight: number; + children: Array; + gap?: number; + isVirtualizationEnabled?: boolean; +} & React.InputHTMLAttributes; + +const bufferedItems = 5; + +/** + * Custom component to implement virtualization of a Ul list + * We used a custom solution in order to provide a semantically correct UL list with accessibility (aria) included. + * Other solutions such as react-window etc couldn't override to use UL lists and pass with ...rest properties + * */ +const Window = forwardRef( + ({ rowHeight, children, gap = 0, isVirtualizationEnabled = true, ...rest }, ref) => { + const [containerRef, { height: containerHeight }] = useElementSize(); + const combinedRefs = useCombinedRefs(containerRef, ref); + const [scrollPosition, setScrollPosition] = React.useState(0); + + // get the children to be renderd + const visibleChildren = React.useMemo(() => { + if (!isVirtualizationEnabled) { + return children.map((child, index) => React.cloneElement(child, {})); + } + const startIndex = Math.max(Math.floor(scrollPosition / rowHeight) - bufferedItems, 0); + const endIndex = Math.min( + Math.ceil((scrollPosition + containerHeight) / rowHeight - 1) + bufferedItems, + children.length - 1 + ); + + return children.slice(startIndex, endIndex + 1).map((child, index) => + React.cloneElement(child, { + style: { + position: 'absolute', + top: (startIndex + index) * rowHeight + index * gap, + height: rowHeight, + left: 0, + right: 0, + lineHeight: `${rowHeight}px`, + }, + }) + ); + }, [children, containerHeight, rowHeight, scrollPosition, gap, isVirtualizationEnabled]); + + const onScroll = React.useMemo( + () => + throttle( + function (e: any) { + setScrollPosition(e.target.scrollTop); + }, + 50, + // eslint-disable-next-line @typescript-eslint/naming-convention + { leading: false } + ), + [] + ); + + return ( +
      + {visibleChildren} +
    + ); + } +); +Window.displayName = 'Window'; + +export default Window; diff --git a/src/components/List/__snapshots__/List.stories.storyshot b/src/components/List/__snapshots__/List.stories.storyshot deleted file mode 100644 index d7b9f81be..000000000 --- a/src/components/List/__snapshots__/List.stories.storyshot +++ /dev/null @@ -1,427 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Storyshots Design System/List List 1`] = ` -.emotion-0 { - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - position: relative; - padding: 0.25rem; -} - -.emotion-1 { - margin: 1rem; -} - -.emotion-2 { - border: 1px solid #e7ebf2; - border-radius: 0.25rem; - width: 650px; -} - -.emotion-3 { - padding-left: 0; - margin-top: 0; - margin-bottom: 0; - border-radius: 0.25rem; - width: 40.625rem; - height: 40.625rem; - overflow: auto; - overflow-x: hidden; -} - -
    -
    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    -
    -`; - -exports[`Storyshots Design System/List List with icons 1`] = ` -.emotion-0 { - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - position: relative; - padding: 0.25rem; -} - -.emotion-1 { - margin: 1rem; -} - -.emotion-2 { - border: 1px solid #e7ebf2; - border-radius: 0.25rem; - width: 650px; -} - -.emotion-3 { - overflow-x: hidden; -} - -.emotion-4 { - position: absolute; - left: 0; - top: 0; - height: 56px; - width: 100%; -} - -.emotion-5 { - position: absolute; - left: 0; - top: 56px; - height: 56px; - width: 100%; -} - -.emotion-6 { - position: absolute; - left: 0; - top: 112px; - height: 56px; - width: 100%; -} - -.emotion-7 { - position: absolute; - left: 0; - top: 168px; - height: 56px; - width: 100%; -} - -.emotion-8 { - position: absolute; - left: 0; - top: 224px; - height: 56px; - width: 100%; -} - -.emotion-9 { - position: absolute; - left: 0; - top: 280px; - height: 56px; - width: 100%; -} - -
    -
    -
    -
    -
    - - - - - - - - - - - - - - - - - - -
    -
    -
    -
    -
    -`; - -exports[`Storyshots Design System/List Virtualized List 1`] = ` -.emotion-0 { - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - position: relative; - padding: 0.25rem; -} - -.emotion-1 { - margin: 1rem; -} - -.emotion-2 { - border: 1px solid #e7ebf2; - border-radius: 0.25rem; - width: 650px; -} - -.emotion-3 { - overflow-x: hidden; -} - -.emotion-4 { - position: absolute; - left: 0; - top: 0; - height: 56px; - width: 100%; -} - -.emotion-5 { - position: absolute; - left: 0; - top: 56px; - height: 56px; - width: 100%; -} - -.emotion-6 { - position: absolute; - left: 0; - top: 112px; - height: 56px; - width: 100%; -} - -.emotion-7 { - position: absolute; - left: 0; - top: 168px; - height: 56px; - width: 100%; -} - -.emotion-8 { - position: absolute; - left: 0; - top: 224px; - height: 56px; - width: 100%; -} - -.emotion-9 { - position: absolute; - left: 0; - top: 280px; - height: 56px; - width: 100%; -} - -.emotion-10 { - position: absolute; - left: 0; - top: 336px; - height: 56px; - width: 100%; -} - -
    -
    -
    -
    -
    - - - - - - - - - - - - - - - - - - - - - -
    -
    -
    -
    -
    -`; diff --git a/src/components/List/components/ListItemAction/ListItemAction.style.ts b/src/components/List/components/ListItemAction/ListItemAction.style.ts new file mode 100644 index 000000000..a51570760 --- /dev/null +++ b/src/components/List/components/ListItemAction/ListItemAction.style.ts @@ -0,0 +1,10 @@ +import { css, SerializedStyles } from '@emotion/react'; +import { rem } from 'theme/utils'; + +export const listItemActionWrapper = () => (): SerializedStyles => + css` + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + `; diff --git a/src/components/List/components/ListItemAction/ListItemAction.tsx b/src/components/List/components/ListItemAction/ListItemAction.tsx new file mode 100644 index 000000000..0a1ab4bf3 --- /dev/null +++ b/src/components/List/components/ListItemAction/ListItemAction.tsx @@ -0,0 +1,11 @@ +import React, { FC } from 'react'; + +import { listItemActionWrapper } from './ListItemAction.style'; + +export type ListItemActionProps = {}; + +const ListItemAction: FC = (props) => { + return
    {props.children}
    ; +}; + +export default ListItemAction; diff --git a/src/components/List/components/ListItemAction/index.ts b/src/components/List/components/ListItemAction/index.ts new file mode 100644 index 000000000..4242ee3ec --- /dev/null +++ b/src/components/List/components/ListItemAction/index.ts @@ -0,0 +1,2 @@ +export { default } from './ListItemAction'; +export * from './ListItemAction'; diff --git a/src/components/List/components/ListItemText/ListItemText.style.ts b/src/components/List/components/ListItemText/ListItemText.style.ts new file mode 100644 index 000000000..d6fe387d9 --- /dev/null +++ b/src/components/List/components/ListItemText/ListItemText.style.ts @@ -0,0 +1,28 @@ +import styled from '@emotion/styled'; + +export const ListItemTextWrapper = styled('div', { target: '' })<{ + isGroupItem?: boolean; + isHighlighted: boolean; +}>` + color: ${({ theme }) => theme.tokens.textColor.get('light.primary')}; + font-size: ${({ theme }) => theme.globals.typography.fontSize.get('4')}; + font-weight: ${({ isGroupItem, isHighlighted }) => + isGroupItem || isHighlighted ? 'bold' : 'initial'}; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: inherit; + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + + strong { + font-weight: bold; + } + + span { + display: block; + } +`; diff --git a/src/components/List/components/ListItemText/ListItemText.tsx b/src/components/List/components/ListItemText/ListItemText.tsx new file mode 100644 index 000000000..21f114605 --- /dev/null +++ b/src/components/List/components/ListItemText/ListItemText.tsx @@ -0,0 +1,18 @@ +import React, { FC } from 'react'; + +import { ListItemTextWrapper } from './ListItemText.style'; + +export type ListItemTextProps = { + description?: string | JSX.Element; +}; + +const ListItemText: FC = (props) => { + return ( + + {props.children} + {props.description &&

    {props.description}

    } +
    + ); +}; + +export default ListItemText; diff --git a/src/components/List/components/ListItemText/index.ts b/src/components/List/components/ListItemText/index.ts new file mode 100644 index 000000000..1b602ac6e --- /dev/null +++ b/src/components/List/components/ListItemText/index.ts @@ -0,0 +1,2 @@ +export { default } from './ListItemText'; +export * from './ListItemText'; diff --git a/src/components/List/components/ListItemWrapper/ListItemWrapper.style.ts b/src/components/List/components/ListItemWrapper/ListItemWrapper.style.ts new file mode 100644 index 000000000..9a2a2778e --- /dev/null +++ b/src/components/List/components/ListItemWrapper/ListItemWrapper.style.ts @@ -0,0 +1,65 @@ +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; +import { rem } from 'theme/utils'; + +import { ListRowSize } from '../../types'; +import { COMPACT_LIST_ITEM_HEIGHT, NORMAL_LIST_ITEM_HEIGHT } from '../../utils'; +import { ListItemTextWrapper } from '../ListItemText/ListItemText.style'; +import { body02, label02, body03, label03 } from 'components/Typography/Typography.config.styles'; + +export const ListItemWrapperStyled = styled('li', { target: '' })<{ + rowSize?: ListRowSize; + isDisabled: boolean; +}>(({ rowSize, isDisabled, theme }) => { + const isCompact = rowSize === 'compact'; + const height = isCompact ? rem(COMPACT_LIST_ITEM_HEIGHT) : rem(NORMAL_LIST_ITEM_HEIGHT); + const padding = css`0 ${theme.globals.spacing.get('5')}`; + const itemTypographyStyle = isCompact ? body03(theme) : body02(theme); + + return css` + background-color: ${theme.globals.colors.white}; + ${ListItemTextWrapper} { + ${itemTypographyStyle}; + } + + span[role='presentation'] { + padding: ${padding}; + min-height: ${height}; + align-items: center; + display: flex; + ${itemTypographyStyle}; + font-weight: ${theme.globals.typography.fontWeight.get('bold')}; + } + &[role='option'] { + padding: ${padding}; + min-height: ${height}; + display: flex; + flex-direction: row; + gap: ${rem(12)}; + + &:hover { + background-color: ${!isDisabled ? theme.utils.getColor('lightGrey', 50) : undefined}; + cursor: ${!isDisabled ? 'pointer' : 'initial'}; + } + } + + &[data-focus-visible] { + background-color: ${theme.utils.getColor('lightGrey', 50)}; + } + + &[aria-selected='true'] { + background-color: ${theme.utils.getColor('blue', 50)}; + ${ListItemTextWrapper} { + color: ${theme.utils.getColor('blue', 550)}; + ${isCompact ? label03(theme) : label02(theme)} + } + + &[data-focus-visible] { + background-color: ${theme.utils.getColor('lightGrey', 50)}; + } + } + + opacity: ${isDisabled ? '0.5' : '1'}; + cursor: ${isDisabled ? 'not-allowed' : 'initial'}; + `; +}); diff --git a/src/components/List/components/ListItemWrapper/ListItemWrapper.tsx b/src/components/List/components/ListItemWrapper/ListItemWrapper.tsx new file mode 100644 index 000000000..e90415079 --- /dev/null +++ b/src/components/List/components/ListItemWrapper/ListItemWrapper.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { TestProps } from 'utils/types'; + +import { ListItemWrapperStyled } from './ListItemWrapper.style'; +import { ListRowSize } from '../../types'; + +export type ListItemProps = { + /** Whether the text of the ListItem is highlighted or not. eg: Filter - Default Value */ + isHighlighted?: boolean; + /** Disabled state */ + isDisabled?: boolean; + /** Search Term to be highlighted in list items */ + searchTerm?: string; + /** Determines if the item is a header of a section */ + isGroupItem?: boolean; + /** @default normal **/ + rowSize?: ListRowSize; +} & TestProps & + Omit, 'value'>; + +const ListItemWrapper = React.forwardRef( + ( + { + children, + rowSize, + isDisabled = false, + isHighlighted = false, + searchTerm, + dataTestId, + ...rest + }, + ref + ) => { + return ( + + {children} + + ); + } +); +ListItemWrapper.displayName = 'ListItem'; + +export default ListItemWrapper; diff --git a/src/components/List/components/ListItemWrapper/README.md b/src/components/List/components/ListItemWrapper/README.md new file mode 100644 index 000000000..b178cfb46 --- /dev/null +++ b/src/components/List/components/ListItemWrapper/README.md @@ -0,0 +1 @@ +a not exported component used to style the li diff --git a/src/components/List/components/ListItemWrapper/index.ts b/src/components/List/components/ListItemWrapper/index.ts new file mode 100644 index 000000000..1c4837b0a --- /dev/null +++ b/src/components/List/components/ListItemWrapper/index.ts @@ -0,0 +1,2 @@ +export { default } from './ListItemWrapper'; +export * from './ListItemWrapper'; diff --git a/src/components/List/index.ts b/src/components/List/index.ts index 31cf8e26e..2ec9f5c30 100644 --- a/src/components/List/index.ts +++ b/src/components/List/index.ts @@ -1,3 +1,7 @@ export { default } from './List'; export * from './List'; +export { default as ListItemText } from './components/ListItemText'; +export { default as ListItemAction } from './components/ListItemAction'; +export { default as ListItem } from './ListItem'; +export { default as ListSection } from './ListSection'; export * from './types'; diff --git a/src/components/List/stories/List.stories.mdx b/src/components/List/stories/List.stories.mdx new file mode 100644 index 000000000..d63ffe76e --- /dev/null +++ b/src/components/List/stories/List.stories.mdx @@ -0,0 +1,137 @@ +import { Meta, Story, Preview, Props } from '@storybook/addon-docs'; +import List, { ListSection, ListItemText, ListItem } from '../index'; +import { FIGMA_URL, Function } from '../../../utils/common'; +import SectionHeader from '../../../storybook/SectionHeader'; + + + + + +- [Overview](#overview) +- [Usage](#usage) +- [Props](#props) +- [Variants](#variants) +- [References](#references) + +## Overview + +The List component is a versatile UI element used to display a collection of items. The List component provides flexibility and customization options to suit various user interface needs. + +## Usage + + + +```js +import { List } from '@orfium/ictinus'; + +const MyComponent = () => { + return {/* Add ListSection(s) or ListItem(s) here */}; +}; +``` + +## Props + + + +## Variants + +### List + + + + + {() => { + return ( + + {new Array(7).fill(undefined).map((__, index) => { + return ( + + Item {index} + + ); + })} + + ); + }} + + + + +### Group List + + + + + {() => { + return ( + + {new Array(3).fill(undefined).map((__, index) => { + return ( + + + Item 1 + + + Item 2 + + + ); + })} + + ); + }} + + + + +### Virtualized List with knobs + +Virtualized List component for large amounts of items. + + + + + {() => { + return ( + + {new Array(1000).fill(undefined).map((__, index) => { + return ( + + Item {index} + + ); + })} + + ); + }} + + + + +### References + +All references for a complete `` with all component mentioned throughout the `` section. + +- [List](?path=/docs/design-system-list-list--list) +- [ListSection](?path=/docs/design-system-list-listsection--normal-list-section) +- [ListItem](?path=/docs/design-system-list-listitem--list-item-compilations) +- [ListItemText](?path=/docs/design-system-list-listitemtext--list-item-text-normal) +- [ListItemAction](?path=/docs/design-system-list-listitemaction--page) diff --git a/src/components/List/stories/ListItem.stories.mdx b/src/components/List/stories/ListItem.stories.mdx new file mode 100644 index 000000000..9a6bab3d9 --- /dev/null +++ b/src/components/List/stories/ListItem.stories.mdx @@ -0,0 +1,145 @@ +import { Meta, Preview, Props, Story } from '@storybook/addon-docs'; +import List, { ListItem, ListItemAction, ListItemText } from '../index'; +import { FIGMA_URL, Function } from '../../../utils/common'; +import SectionHeader from '../../../storybook/SectionHeader'; +import Radio from '../../Radio'; +import CheckBox from '../../CheckBox'; +import Switch from '../../Switch'; +import Avatar from '../../Avatar'; +import Box from '../../Box'; +import { useState } from 'react'; + + + + + +- [Overview](#overview) +- [Usage](#usage) +- [Props](#props) +- [Variants](#variants) +- [References](#references) + +## Overview + +The ListItem component represents an individual item in the List. It can contain either ListItemText or ListItemAction, both, or custom component. + +## Usage + + + +```js +import { List, ListItem } from '@orfium/ictinus'; + +const MyComponent = () => { + return ( + + ... + ... + + ); +}; +``` + +## Props + + + +## Variants + +### List Item Compilations + + + + + {() => { + const [selectedKeys, setSelectedKeys] = useState(new Set()); + return ( + <> + + + + + + + Option Radio + + + + + + + + + + Option Checkbox + + + + + + + Option Switch + + {}} /> + + + + + + + + + + + Option Icon/Avatar + + + + + + + Text only + + + + + + + Compact size item + + + + + ); + }} + + + + +### References + +All references for a complete `` with all component mentioned throughout the `` section. + +- [List](?path=/docs/design-system-list-list--list) +- [ListSection](?path=/docs/design-system-list-listsection--normal-list-section) +- [ListItem](?path=/docs/design-system-list-listitem--list-item-compilations) +- [ListItemText](?path=/docs/design-system-list-listitemtext--list-item-text-normal) +- [ListItemAction](?path=/docs/design-system-list-listitemaction--page) diff --git a/src/components/List/stories/ListItemAction.stories.mdx b/src/components/List/stories/ListItemAction.stories.mdx new file mode 100644 index 000000000..b1361e85c --- /dev/null +++ b/src/components/List/stories/ListItemAction.stories.mdx @@ -0,0 +1,68 @@ +import { Meta } from '@storybook/addon-docs'; +import { ListItemAction } from '../index'; +import { FIGMA_URL } from '../../../utils/common'; +import SectionHeader from '../../../storybook/SectionHeader'; + + + + + +- [Overview](#overview) +- [Usage](#usage) +- [Props](#props) +- [References](#references) + +## Overview + +The ListItemAction component represents an action or a set of actions associated with a ListItem. It can contain buttons, icons, or any other interactive elements. + +## Usage + + + +```js +import { List, ListItem, ListItemAction, Radio, Checkbox } from '@orfium/ictinus'; + +const MyComponent = () => { + return ( + + + + + + + + + + + + + ); +}; +``` + +## Props + +No props defined for this component. + +### References + +All references for a complete `` with all component mentioned throughout the `` section. + +- [List](?path=/docs/design-system-list-list--list) +- [ListSection](?path=/docs/design-system-list-listsection--normal-list-section) +- [ListItem](?path=/docs/design-system-list-listitem--list-item-compilations) +- [ListItemText](?path=/docs/design-system-list-listitemtext--list-item-text-normal) +- [ListItemAction](?path=/docs/design-system-list-listitemaction--page) diff --git a/src/components/List/stories/ListItemText.stories.mdx b/src/components/List/stories/ListItemText.stories.mdx new file mode 100644 index 000000000..f1ebe8bfd --- /dev/null +++ b/src/components/List/stories/ListItemText.stories.mdx @@ -0,0 +1,120 @@ +import { Meta, Story, Preview, Props } from '@storybook/addon-docs'; +import List, { ListItemText, ListItem } from '../index'; +import { FIGMA_URL, Function } from '../../../utils/common'; +import SectionHeader from '../../../storybook/SectionHeader'; +import Stack from '../../storyUtils/Stack'; + + + + + +- [Overview](#overview) +- [Usage](#usage) +- [Props](#props) +- [Variants](#variants) +- [References](#references) + +## Overview + +The ListItemText component is used within a ListItem to display textual content with a primary and a secondary text. + +## Usage + + + +```js +import { List, ListItem, ListItemText } from '@orfium/ictinus'; + +const MyComponent = () => { + return ( + + + Item 1 + + + Item 2 + + + ); +}; +``` + +## Props + + + +## Variants + +### ListItemText normal + + + + + + {() => { + return ( + + + Item 1 + + + Item 2 + + + ); + }} + + + + + +### ListItemText with secondary text + + + + + + {() => { + return ( + + + + Item 1 + + + + + Item 2 + + + + ); + }} + + + + + +### References + +All references for a complete `` with all component mentioned throughout the `` section. + +- [List](?path=/docs/design-system-list-list--list) +- [ListSection](?path=/docs/design-system-list-listsection--normal-list-section) +- [ListItem](?path=/docs/design-system-list-listitem--list-item-compilations) +- [ListItemText](?path=/docs/design-system-list-listitemtext--list-item-text-normal) +- [ListItemAction](?path=/docs/design-system-list-listitemaction--page) diff --git a/src/components/List/stories/ListSection.stories.mdx b/src/components/List/stories/ListSection.stories.mdx new file mode 100644 index 000000000..59faec50d --- /dev/null +++ b/src/components/List/stories/ListSection.stories.mdx @@ -0,0 +1,143 @@ +import { Meta, Story, Preview, Props } from '@storybook/addon-docs'; +import List, { ListSection, ListItem, ListItemText } from '../index'; +import { FIGMA_URL, Function } from '../../../utils/common'; +import SectionHeader from '../../../storybook/SectionHeader'; +import Stack from '../../storyUtils/Stack'; + + + + + +- [Overview](#overview) +- [Usage](#usage) +- [Props](#props) +- [Variants](#variants) +- [References](#references) + +## Overview + +The ListSection component is used to group related items within the List. + +## Usage + + + +```js +import { List, ListSection, ListItem } from '@orfium/ictinus'; + +const MyComponent = () => { + return ( + + + ... + ... + + + + ... + ... + + + ); +}; +``` + +## Props + + + +## Variants + +### Normal ListSection + + + + + + {() => { + return ( + + + + ... + + + ... + + + + + ... + + + ... + + + + ); + }} + + + + + +### Compact ListSection + + + + + + {() => { + return ( + + + + ... + + + ... + + + + + ... + + + ... + + + + ); + }} + + + + + +### References + +All references for a complete `` with all component mentioned throughout the `` section. + +- [List](?path=/docs/design-system-list-list--list) +- [ListSection](?path=/docs/design-system-list-listsection--normal-list-section) +- [ListItem](?path=/docs/design-system-list-listitem--list-item-compilations) +- [ListItemText](?path=/docs/design-system-list-listitemtext--list-item-text-normal) +- [ListItemAction](?path=/docs/design-system-list-listitemaction--page) diff --git a/src/components/List/stories/__snapshots__/List.stories.storyshot b/src/components/List/stories/__snapshots__/List.stories.storyshot new file mode 100644 index 000000000..0cb2fca5a --- /dev/null +++ b/src/components/List/stories/__snapshots__/List.stories.storyshot @@ -0,0 +1,1267 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Design System/List/List Group List 1`] = ` +.emotion-0 { + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + position: relative; + padding: 0.25rem; +} + +.emotion-1 { + margin: 1rem; +} + +.emotion-2 { + border: 1px solid #e7ebf2; + border-radius: 0.25rem; + width: undefinedpx; +} + +.emotion-3 { + padding-left: 0; + margin-top: 0; + margin-bottom: 0; + border-radius: 0.25rem; + width: 100%; + height: auto; + overflow: auto; + overflow-x: hidden; + background: white; +} + +.emotion-4 { + background-color: white; + opacity: 1; + cursor: initial; +} + +.emotion-4 . { + font-family: Roboto; + font-weight: 400; + line-height: 1.25rem; + font-size: 0.875rem; + letter-spacing: 0.015625rem; +} + +.emotion-4 span[role='presentation'] { + padding: 0 0.75rem; + min-height: 3.25rem; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + font-family: Roboto; + font-weight: 400; + line-height: 1.25rem; + font-size: 0.875rem; + letter-spacing: 0.015625rem; + font-weight: 700; +} + +.emotion-4[role='option'] { + padding: 0 0.75rem; + min-height: 3.25rem; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + gap: 0.75rem; +} + +.emotion-4[role='option']:hover { + background-color: #f3f5f8; + cursor: pointer; +} + +.emotion-4[data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-4[aria-selected='true'] { + background-color: #e7eefe; +} + +.emotion-4[aria-selected='true'] . { + color: #1451dc; + font-family: Roboto; + font-weight: 500; + line-height: 1.25rem; + font-size: 0.875rem; + letter-spacing: 0.015625rem; +} + +.emotion-4[aria-selected='true'][data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-7 { + color: #212332; + font-size: 1rem; + font-weight: initial; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: inherit; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; +} + +.emotion-7 strong { + font-weight: bold; +} + +.emotion-7 span { + display: block; +} + +
    +
    +
    +
    +
    +
      +
    • + + Group 0 + +
        +
      • +
        + + Item 1 + +
        +
      • +
      • +
        + + Item 2 + +
        +
      • +
      +
    • +
    • + + Group 1 + +
        +
      • +
        + + Item 1 + +
        +
      • +
      • +
        + + Item 2 + +
        +
      • +
      +
    • +
    • + + Group 2 + +
        +
      • +
        + + Item 1 + +
        +
      • +
      • +
        + + Item 2 + +
        +
      • +
      +
    • +
    +
    +
    +
    +
    +
    +`; + +exports[`Storyshots Design System/List/List List 1`] = ` +.emotion-0 { + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + position: relative; + padding: 0.25rem; +} + +.emotion-1 { + margin: 1rem; +} + +.emotion-2 { + border: 1px solid #e7ebf2; + border-radius: 0.25rem; + width: undefinedpx; +} + +.emotion-3 { + padding-left: 0; + margin-top: 0; + margin-bottom: 0; + border-radius: 0.25rem; + width: 100%; + height: auto; + overflow: auto; + overflow-x: hidden; + background: white; +} + +.emotion-4 { + background-color: white; + opacity: 1; + cursor: initial; +} + +.emotion-4 . { + font-family: Roboto; + font-weight: 400; + line-height: 1.25rem; + font-size: 0.875rem; + letter-spacing: 0.015625rem; +} + +.emotion-4 span[role='presentation'] { + padding: 0 0.75rem; + min-height: 3.25rem; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + font-family: Roboto; + font-weight: 400; + line-height: 1.25rem; + font-size: 0.875rem; + letter-spacing: 0.015625rem; + font-weight: 700; +} + +.emotion-4[role='option'] { + padding: 0 0.75rem; + min-height: 3.25rem; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + gap: 0.75rem; +} + +.emotion-4[role='option']:hover { + background-color: #f3f5f8; + cursor: pointer; +} + +.emotion-4[data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-4[aria-selected='true'] { + background-color: #e7eefe; +} + +.emotion-4[aria-selected='true'] . { + color: #1451dc; + font-family: Roboto; + font-weight: 500; + line-height: 1.25rem; + font-size: 0.875rem; + letter-spacing: 0.015625rem; +} + +.emotion-4[aria-selected='true'][data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-5 { + color: #212332; + font-size: 1rem; + font-weight: initial; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: inherit; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; +} + +.emotion-5 strong { + font-weight: bold; +} + +.emotion-5 span { + display: block; +} + +
    +
    +
    +
    +
    +
      +
    • +
      + + Item + 0 + +
      +
    • +
    • +
      + + Item + 1 + +
      +
    • +
    • +
      + + Item + 2 + +
      +
    • +
    • +
      + + Item + 3 + +
      +
    • +
    • +
      + + Item + 4 + +
      +
    • +
    • +
      + + Item + 5 + +
      +
    • +
    • +
      + + Item + 6 + +
      +
    • +
    +
    +
    +
    +
    +
    +`; + +exports[`Storyshots Design System/List/List Virtualized List 1`] = ` +.emotion-0 { + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + position: relative; + padding: 0.25rem; +} + +.emotion-1 { + margin: 1rem; +} + +.emotion-2 { + border: 1px solid #e7ebf2; + border-radius: 0.25rem; + width: undefinedpx; +} + +.emotion-3 { + padding-left: 0; + margin-top: 0; + margin-bottom: 0; + border-radius: 0.25rem; + width: 100%; + height: 28.125rem; + overflow: auto; + overflow-x: hidden; + background: white; +} + +.emotion-4 { + background-color: white; + opacity: 1; + cursor: initial; +} + +.emotion-4 . { + font-family: Roboto; + font-weight: 400; + line-height: 1.25rem; + font-size: 0.875rem; + letter-spacing: 0.015625rem; +} + +.emotion-4 span[role='presentation'] { + padding: 0 0.75rem; + min-height: 3.25rem; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + font-family: Roboto; + font-weight: 400; + line-height: 1.25rem; + font-size: 0.875rem; + letter-spacing: 0.015625rem; + font-weight: 700; +} + +.emotion-4[role='option'] { + padding: 0 0.75rem; + min-height: 3.25rem; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + gap: 0.75rem; +} + +.emotion-4[role='option']:hover { + background-color: #f3f5f8; + cursor: pointer; +} + +.emotion-4[data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-4[aria-selected='true'] { + background-color: #e7eefe; +} + +.emotion-4[aria-selected='true'] . { + color: #1451dc; + font-family: Roboto; + font-weight: 500; + line-height: 1.25rem; + font-size: 0.875rem; + letter-spacing: 0.015625rem; +} + +.emotion-4[aria-selected='true'][data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-5 { + color: #212332; + font-size: 1rem; + font-weight: initial; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: inherit; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; +} + +.emotion-5 strong { + font-weight: bold; +} + +.emotion-5 span { + display: block; +} + +
    +
    +
    +
    +
    +
      +
    • +
      + + Item + 0 + +
      +
    • +
    • +
      + + Item + 1 + +
      +
    • +
    • +
      + + Item + 2 + +
      +
    • +
    • +
      + + Item + 3 + +
      +
    • +
    • +
      + + Item + 4 + +
      +
    • +
    +
    +
    +
    +
    +
    +`; diff --git a/src/components/List/stories/__snapshots__/ListItem.stories.storyshot b/src/components/List/stories/__snapshots__/ListItem.stories.storyshot new file mode 100644 index 000000000..2c7c742de --- /dev/null +++ b/src/components/List/stories/__snapshots__/ListItem.stories.storyshot @@ -0,0 +1,1216 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Design System/List/ListItem List Item Compilations 1`] = ` +.emotion-0 { + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + position: relative; + padding: 0.25rem; +} + +.emotion-1 { + margin: 1rem; +} + +.emotion-2 { + margin-bottom: 0.25rem; +} + +.emotion-3 { + border: 1px solid #e7ebf2; + border-radius: 0.25rem; + width: undefinedpx; +} + +.emotion-4 { + padding-left: 0; + margin-top: 0; + margin-bottom: 0; + border-radius: 0.25rem; + width: 100%; + height: auto; + overflow: auto; + overflow-x: hidden; + background: white; +} + +.emotion-5 { + background-color: white; + opacity: 1; + cursor: initial; +} + +.emotion-5 . { + font-family: Roboto; + font-weight: 400; + line-height: 1.25rem; + font-size: 0.875rem; + letter-spacing: 0.015625rem; +} + +.emotion-5 span[role='presentation'] { + padding: 0 0.75rem; + min-height: 3.25rem; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + font-family: Roboto; + font-weight: 400; + line-height: 1.25rem; + font-size: 0.875rem; + letter-spacing: 0.015625rem; + font-weight: 700; +} + +.emotion-5[role='option'] { + padding: 0 0.75rem; + min-height: 3.25rem; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + gap: 0.75rem; +} + +.emotion-5[role='option']:hover { + background-color: #f3f5f8; + cursor: pointer; +} + +.emotion-5[data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-5[aria-selected='true'] { + background-color: #e7eefe; +} + +.emotion-5[aria-selected='true'] . { + color: #1451dc; + font-family: Roboto; + font-weight: 500; + line-height: 1.25rem; + font-size: 0.875rem; + letter-spacing: 0.015625rem; +} + +.emotion-5[aria-selected='true'][data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-6 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; +} + +.emotion-7 { + position: relative; + border-radius: 50%; + width: 2.25rem; + height: 2.25rem; + color: #175bf5; + border: 0; + opacity: 1; + cursor: pointer; + margin: 0; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + outline: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + vertical-align: middle; + -moz-appearance: none; + -webkit-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; + -webkit-text-decoration: none; + text-decoration: none; + -webkit-appearance: none; + -webkit-tap-highlight-color: transparent; +} + +.emotion-8 { + top: 0; + left: 0; + width: 100%; + cursor: inherit; + height: 100%; + margin: 0; + padding: 0; + z-index: 1; + position: absolute; + opacity: 0; +} + +.emotion-8:disabled { + cursor: default; +} + +.emotion-9 { + position: relative; + border-radius: 50%; + width: 1.5rem; + height: 1.5rem; + -webkit-transition: box-shadow 0.3s ease; + transition: box-shadow 0.3s ease; +} + +.emotion-10 { + -webkit-transition: all 0.2s ease; + transition: all 0.2s ease; + border-radius: 50%; + width: 100%; + height: 100%; + box-sizing: border-box; + position: absolute; +} + +.emotion-10:before { + content: ''; + display: inline-block; + box-sizing: border-box; + margin: 0.125rem 0.75rem 0.125rem 0.125rem; + border: solid 2px #b7c3d8; + border-radius: 50%; + width: 1.25rem; + height: 1.25rem; + vertical-align: top; + -webkit-transition: border-color 0.2s; + transition: border-color 0.2s; +} + +.emotion-10:after { + content: ''; + display: block; + position: absolute; + top: 0.0625rem; + left: 0.0625rem; + border-radius: 50%; + width: 0.75rem; + height: 0.75rem; + background-color: #b7c3d8; + -webkit-transform: translate(5px, 5px) scale(0); + -moz-transform: translate(5px, 5px) scale(0); + -ms-transform: translate(5px, 5px) scale(0); + transform: translate(5px, 5px) scale(0); + -webkit-transition: -webkit-transform 0.2s; + transition: transform 0.2s; +} + +.emotion-11 { + position: absolute; + border-radius: 50%; + width: 1.5rem; + height: 1.5rem; + -webkit-transition: all 0.2s ease; + transition: all 0.2s ease; +} + +.emotion-12 { + color: #212332; + font-size: 1rem; + font-weight: initial; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: inherit; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; +} + +.emotion-12 strong { + font-weight: bold; +} + +.emotion-12 span { + display: block; +} + +.emotion-18 { + opacity: 1; + -webkit-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.emotion-19 { + border-radius: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + width: 3rem; + height: 3rem; + -webkit-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + position: relative; +} + +.emotion-19:before { + display: none; + border-radius: 100%; + -webkit-transition: all 0.2s; + transition: all 0.2s; + content: ' '; + width: 3rem; + height: 3rem; + position: absolute; +} + +.emotion-19:hover:before { + display: block; + background: rgba(0, 0, 0, 0.05); +} + +.emotion-20 { + border: 0; + border-radius: 0.125rem; + width: 1.5rem; + height: 1.5rem; + position: absolute; + opacity: 0; +} + +.emotion-20+label { + position: relative; + cursor: pointer; + padding: 0; +} + +.emotion-20+label:before { + content: ''; + -webkit-transition: all 0.2s; + transition: all 0.2s; + display: inline-block; + vertical-align: text-top; + width: 1.5rem; + height: 1.5rem; + background: inherit; + box-shadow: inset 0px 0px 0px 0.125rem #b7c3d8; + border-radius: 0.25rem; +} + +.emotion-20:disabled+label { + cursor: not-allowed; +} + +.emotion-21 span { + padding: 0; +} + +.emotion-21 svg { + position: absolute; + top: 0; + display: none; +} + +.emotion-22 { + padding: 0.125rem; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; +} + +.emotion-23 { + fill: white; + width: 1.5rem; + height: 1.5rem; +} + +.emotion-23 path { + fill: white; +} + +.emotion-31 { + display: -webkit-box; + display: -moz-box; + display: -ms-flexbox; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.emotion-31 .react-switch-handle { + cursor: unset!important; + border: 2px solid #b7c3d8!important; + box-sizing: border-box!important; + -webkit-transform: translateX(0)!important; + -moz-transform: translateX(0)!important; + -ms-transform: translateX(0)!important; + transform: translateX(0)!important; +} + +.emotion-31 .react-switch-handle:hover { + box-shadow: rgba(14,14,23,0.1) 0 0 0 5px; + background: #dbe1eb!important; +} + +.emotion-31 .react-switch-bg { + cursor: unset!important; + margin-right: 0!important; + margin-left: 0!important; +} + +.emotion-31 .react-switch-bg:hover~.react-switch-handle { + box-shadow: rgba(14,14,23,0.1) 0 0 0 5px; + background: #dbe1eb!important; +} + +.emotion-37 { + display: -webkit-box; + display: -moz-box; + display: -ms-flexbox; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + width: 1.25rem; + height: 1.25rem; + border-radius: 6.25rem; + border: 0.0625rem solid; + border-color: #c2c8ff; + box-sizing: border-box; + background-color: #d0defd; + color: #1451dc; + overflow: hidden; + position: relative; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; + font-size: 0.625rem; + font-weight: 500; + line-height: 1.25rem; + letter-spacing: 0rem; +} + +.emotion-37 img { + border-radius: #c2c8ff; + width: 100%; + height: 100%; +} + +.emotion-39 { + fill: #1451dc; + width: 1rem; + height: 1rem; +} + +.emotion-39 path { + fill: #1451dc; +} + +.emotion-49 { + background-color: white; + opacity: 1; + cursor: initial; +} + +.emotion-49 . { + font-family: Roboto; + font-weight: 400; + line-height: 1rem; + font-size: 0.75rem; + letter-spacing: 0.015625rem; +} + +.emotion-49 span[role='presentation'] { + padding: 0 0.75rem; + min-height: 2.5rem; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + font-family: Roboto; + font-weight: 400; + line-height: 1rem; + font-size: 0.75rem; + letter-spacing: 0.015625rem; + font-weight: 700; +} + +.emotion-49[role='option'] { + padding: 0 0.75rem; + min-height: 2.5rem; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + gap: 0.75rem; +} + +.emotion-49[role='option']:hover { + background-color: #f3f5f8; + cursor: pointer; +} + +.emotion-49[data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-49[aria-selected='true'] { + background-color: #e7eefe; +} + +.emotion-49[aria-selected='true'] . { + color: #1451dc; + font-family: Roboto; + font-weight: 500; + line-height: 1rem; + font-size: 0.75rem; + letter-spacing: 0.015625rem; +} + +.emotion-49[aria-selected='true'][data-focus-visible] { + background-color: #f3f5f8; +} + +
    +
    +
    +
    +
    +
    +
      +
    • +
      + + + + + + + +
      +
      + + Option Radio + +
      +
    • +
    +
    +
    +
    +
    +
    +
    +
    +
    +
      +
    • +
      + + + + + + +
      +
      + + Option Checkbox + +
      +
    • +
    +
    +
    +
    +
    +
    +
    +
    +
    +
      +
    • +
      + + Option Switch + +
      +
      +
      +
      +
      +
      + +
      +
      +
      +
    • +
    +
    +
    +
    +
    +
    +
    +
    +
    +
      +
    • +
      +
      + + + +
      +
      +
      + + Option Icon/Avatar + +
      +
    • +
    +
    +
    +
    +
    +
    +
    +
    +
    +
      +
    • +
      + + Text only + +

      + Secondary Help Text +

      +
      +
    • +
    +
    +
    +
    +
    +
    +
    +
    +
    +
      +
    • +
      + + Compact size item + +
      +
    • +
    +
    +
    +
    +
    +
    +
    +`; diff --git a/src/components/List/stories/__snapshots__/ListItemText.stories.storyshot b/src/components/List/stories/__snapshots__/ListItemText.stories.storyshot new file mode 100644 index 000000000..4a4d48520 --- /dev/null +++ b/src/components/List/stories/__snapshots__/ListItemText.stories.storyshot @@ -0,0 +1,565 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Design System/List/ListItemText ListItemText normal 1`] = ` +.emotion-0 { + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + position: relative; + padding: 0.25rem; +} + +.emotion-1 { + margin: 1rem; +} + +.emotion-2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-flex-wrap: wrap; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; +} + +.emotion-3 { + border: 1px solid #e7ebf2; + border-radius: 0.25rem; + width: undefinedpx; +} + +.emotion-4 { + padding-left: 0; + margin-top: 0; + margin-bottom: 0; + border-radius: 0.25rem; + width: 100%; + height: auto; + overflow: auto; + overflow-x: hidden; + background: white; +} + +.emotion-5 { + background-color: white; + opacity: 1; + cursor: initial; +} + +.emotion-5 . { + font-family: Roboto; + font-weight: 400; + line-height: 1.25rem; + font-size: 0.875rem; + letter-spacing: 0.015625rem; +} + +.emotion-5 span[role='presentation'] { + padding: 0 0.75rem; + min-height: 3.25rem; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + font-family: Roboto; + font-weight: 400; + line-height: 1.25rem; + font-size: 0.875rem; + letter-spacing: 0.015625rem; + font-weight: 700; +} + +.emotion-5[role='option'] { + padding: 0 0.75rem; + min-height: 3.25rem; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + gap: 0.75rem; +} + +.emotion-5[role='option']:hover { + background-color: #f3f5f8; + cursor: pointer; +} + +.emotion-5[data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-5[aria-selected='true'] { + background-color: #e7eefe; +} + +.emotion-5[aria-selected='true'] . { + color: #1451dc; + font-family: Roboto; + font-weight: 500; + line-height: 1.25rem; + font-size: 0.875rem; + letter-spacing: 0.015625rem; +} + +.emotion-5[aria-selected='true'][data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-6 { + color: #212332; + font-size: 1rem; + font-weight: initial; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: inherit; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; +} + +.emotion-6 strong { + font-weight: bold; +} + +.emotion-6 span { + display: block; +} + +
    +
    +
    +
    +
    +
    +
    +
      +
    • +
      + + Item 1 + +
      +
    • +
    • +
      + + Item 2 + +
      +
    • +
    +
    +
    +
    +
    +
    +
    +
    +`; + +exports[`Storyshots Design System/List/ListItemText ListItemText with secondary text 1`] = ` +.emotion-0 { + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + position: relative; + padding: 0.25rem; +} + +.emotion-1 { + margin: 1rem; +} + +.emotion-2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-flex-wrap: wrap; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; +} + +.emotion-3 { + border: 1px solid #e7ebf2; + border-radius: 0.25rem; + width: undefinedpx; +} + +.emotion-4 { + padding-left: 0; + margin-top: 0; + margin-bottom: 0; + border-radius: 0.25rem; + width: 100%; + height: auto; + overflow: auto; + overflow-x: hidden; + background: white; +} + +.emotion-5 { + background-color: white; + opacity: 1; + cursor: initial; +} + +.emotion-5 . { + font-family: Roboto; + font-weight: 400; + line-height: 1.25rem; + font-size: 0.875rem; + letter-spacing: 0.015625rem; +} + +.emotion-5 span[role='presentation'] { + padding: 0 0.75rem; + min-height: 3.25rem; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + font-family: Roboto; + font-weight: 400; + line-height: 1.25rem; + font-size: 0.875rem; + letter-spacing: 0.015625rem; + font-weight: 700; +} + +.emotion-5[role='option'] { + padding: 0 0.75rem; + min-height: 3.25rem; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + gap: 0.75rem; +} + +.emotion-5[role='option']:hover { + background-color: #f3f5f8; + cursor: pointer; +} + +.emotion-5[data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-5[aria-selected='true'] { + background-color: #e7eefe; +} + +.emotion-5[aria-selected='true'] . { + color: #1451dc; + font-family: Roboto; + font-weight: 500; + line-height: 1.25rem; + font-size: 0.875rem; + letter-spacing: 0.015625rem; +} + +.emotion-5[aria-selected='true'][data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-6 { + color: #212332; + font-size: 1rem; + font-weight: initial; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: inherit; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; +} + +.emotion-6 strong { + font-weight: bold; +} + +.emotion-6 span { + display: block; +} + +
    +
    +
    +
    +
    +
    +
    +
      +
    • +
      + + Item 1 + +

      + This is a secondary text on this item +

      +
      +
    • +
    • +
      + + Item 2 + +

      + This is a secondary text on this item +

      +
      +
    • +
    +
    +
    +
    +
    +
    +
    +
    +`; diff --git a/src/components/List/stories/__snapshots__/ListSection.stories.storyshot b/src/components/List/stories/__snapshots__/ListSection.stories.storyshot new file mode 100644 index 000000000..2beeeda68 --- /dev/null +++ b/src/components/List/stories/__snapshots__/ListSection.stories.storyshot @@ -0,0 +1,755 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Design System/List/ListSection Compact ListSection 1`] = ` +.emotion-0 { + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + position: relative; + padding: 0.25rem; +} + +.emotion-1 { + margin: 1rem; +} + +.emotion-2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-flex-wrap: wrap; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; +} + +.emotion-3 { + border: 1px solid #e7ebf2; + border-radius: 0.25rem; + width: undefinedpx; +} + +.emotion-4 { + padding-left: 0; + margin-top: 0; + margin-bottom: 0; + border-radius: 0.25rem; + width: 100%; + height: auto; + overflow: auto; + overflow-x: hidden; + background: white; +} + +.emotion-5 { + background-color: white; + opacity: 1; + cursor: initial; +} + +.emotion-5 . { + font-family: Roboto; + font-weight: 400; + line-height: 1rem; + font-size: 0.75rem; + letter-spacing: 0.015625rem; +} + +.emotion-5 span[role='presentation'] { + padding: 0 0.75rem; + min-height: 2.5rem; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + font-family: Roboto; + font-weight: 400; + line-height: 1rem; + font-size: 0.75rem; + letter-spacing: 0.015625rem; + font-weight: 700; +} + +.emotion-5[role='option'] { + padding: 0 0.75rem; + min-height: 2.5rem; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + gap: 0.75rem; +} + +.emotion-5[role='option']:hover { + background-color: #f3f5f8; + cursor: pointer; +} + +.emotion-5[data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-5[aria-selected='true'] { + background-color: #e7eefe; +} + +.emotion-5[aria-selected='true'] . { + color: #1451dc; + font-family: Roboto; + font-weight: 500; + line-height: 1rem; + font-size: 0.75rem; + letter-spacing: 0.015625rem; +} + +.emotion-5[aria-selected='true'][data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-8 { + color: #212332; + font-size: 1rem; + font-weight: initial; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: inherit; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; +} + +.emotion-8 strong { + font-weight: bold; +} + +.emotion-8 span { + display: block; +} + +
    +
    +
    +
    +
    +
    +
    +
      +
    • + + Section 1 + +
        +
      • +
        + + ... + +
        +
      • +
      • +
        + + ... + +
        +
      • +
      +
    • +
    • + + Section 2 + +
        +
      • +
        + + ... + +
        +
      • +
      • +
        + + ... + +
        +
      • +
      +
    • +
    +
    +
    +
    +
    +
    +
    +
    +`; + +exports[`Storyshots Design System/List/ListSection Normal ListSection 1`] = ` +.emotion-0 { + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + position: relative; + padding: 0.25rem; +} + +.emotion-1 { + margin: 1rem; +} + +.emotion-2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-flex-wrap: wrap; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; +} + +.emotion-3 { + border: 1px solid #e7ebf2; + border-radius: 0.25rem; + width: undefinedpx; +} + +.emotion-4 { + padding-left: 0; + margin-top: 0; + margin-bottom: 0; + border-radius: 0.25rem; + width: 100%; + height: auto; + overflow: auto; + overflow-x: hidden; + background: white; +} + +.emotion-5 { + background-color: white; + opacity: 1; + cursor: initial; +} + +.emotion-5 . { + font-family: Roboto; + font-weight: 400; + line-height: 1.25rem; + font-size: 0.875rem; + letter-spacing: 0.015625rem; +} + +.emotion-5 span[role='presentation'] { + padding: 0 0.75rem; + min-height: 3.25rem; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + font-family: Roboto; + font-weight: 400; + line-height: 1.25rem; + font-size: 0.875rem; + letter-spacing: 0.015625rem; + font-weight: 700; +} + +.emotion-5[role='option'] { + padding: 0 0.75rem; + min-height: 3.25rem; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + gap: 0.75rem; +} + +.emotion-5[role='option']:hover { + background-color: #f3f5f8; + cursor: pointer; +} + +.emotion-5[data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-5[aria-selected='true'] { + background-color: #e7eefe; +} + +.emotion-5[aria-selected='true'] . { + color: #1451dc; + font-family: Roboto; + font-weight: 500; + line-height: 1.25rem; + font-size: 0.875rem; + letter-spacing: 0.015625rem; +} + +.emotion-5[aria-selected='true'][data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-8 { + color: #212332; + font-size: 1rem; + font-weight: initial; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: inherit; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; +} + +.emotion-8 strong { + font-weight: bold; +} + +.emotion-8 span { + display: block; +} + +
    +
    +
    +
    +
    +
    +
    +
      +
    • + + Section 1 + +
        +
      • +
        + + ... + +
        +
      • +
      • +
        + + ... + +
        +
      • +
      +
    • +
    • + + Section 2 + +
        +
      • +
        + + ... + +
        +
      • +
      • +
        + + ... + +
        +
      • +
      +
    • +
    +
    +
    +
    +
    +
    +
    +
    +`; diff --git a/src/components/List/types.ts b/src/components/List/types.ts index 4cd3d7c81..452661999 100644 --- a/src/components/List/types.ts +++ b/src/components/List/types.ts @@ -1,7 +1,12 @@ +import { Key } from 'react'; + import { SelectOption } from 'components/Select'; +export type ListSelection = Set; +export type ListSelected = 'all' | Iterable; + export type ListItemType = SelectOption; -export type ListRowSize = 'small' | 'normal'; +export type ListRowSize = 'compact' | 'normal'; export type SelectHandlerType = (option: ListItemType) => void; diff --git a/src/components/List/utils.tsx b/src/components/List/utils.tsx index b72fc879f..ab4bc9a06 100644 --- a/src/components/List/utils.tsx +++ b/src/components/List/utils.tsx @@ -1,11 +1,7 @@ import React from 'react'; -import Highlighter from 'react-highlight-words'; -import { listLabel, listLabelHelperText, listLabelWithHelper } from './List.style'; -import { ListItemType } from './types'; -import { FilterOption } from '../Filter'; -import Icon from '../Icon'; -import { SelectOption } from '../Select'; +// @TODO this needs to be used on Select/Filter level now to have such functionality +// import Highlighter from 'react-highlight-words'; /** For this amount of List Items the list of Filter will be non-virtualized */ export const MAX_NON_VIRTUALIZED_ITEMS_FILTER = 6; @@ -15,59 +11,5 @@ export const MAX_NON_VIRTUALIZED_ITEMS_SELECT = 5; export const MAX_LARGE_HEIGHT = 277; export const MAX_SMALL_HEIGHT = 265; -export const isSelected = ({ - item, - selectedItem, -}: { - item: ListItemType; - selectedItem: ListItemType | undefined; -}): boolean => { - if (item && React.isValidElement(item)) { - return false; - } - const checkIfItemHasValue = (item: ListItemType) => { - return item.value; - }; - const itemValue = checkIfItemHasValue(item); - const selectedItemValue = selectedItem ? checkIfItemHasValue(selectedItem) : null; - - return itemValue === selectedItemValue; -}; - -const renderLabelWithHelperText = (content: SelectOption | FilterOption) => { - if (content?.label && 'helperText' in content && content?.helperText) { - return ( -
    -
    {content.label}
    -
    {content.helperText}
    -
    - ); - } - - return content.label; -}; - -export const renderContent = (content: ListItemType, searchTerm?: string) => { - if (searchTerm && 'label' in content && content?.label) { - return ( - - ); - } - - if ('label' in content && content?.label) { - return ( - <> -
    {renderLabelWithHelperText(content)}
    - {content?.iconProps && } - - ); - } - - return content; -}; +export const COMPACT_LIST_ITEM_HEIGHT = 40; +export const NORMAL_LIST_ITEM_HEIGHT = 52; diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx index 5dc9ca541..f975aa1a9 100644 --- a/src/components/Menu/Menu.tsx +++ b/src/components/Menu/Menu.tsx @@ -1,21 +1,17 @@ import useTheme from 'hooks/useTheme'; -import { useTypeColorToColorMatch } from 'hooks/useTypeColorToColorMatch'; -import { isEmpty } from 'lodash'; +import { head, isEmpty } from 'lodash'; import * as React from 'react'; import { EventProps } from 'utils/common'; -import { AcceptedColorComponentTypes } from 'utils/themeFunctions'; import { wrapperStyle } from './Menu.style'; import { TestProps } from '../../utils/types'; import Button from '../Button'; -import { defineBackgroundColor } from '../Button/utils'; -import Icon from '../Icon'; import { AcceptedIconNames } from '../Icon/types'; import ClickAwayListener from '../utils/ClickAwayListener'; import { optionsStyle, MenuPositionAllowed } from '../utils/DropdownOptions'; -import Avatar, { AvatarColors } from 'components/Avatar'; +import { AvatarColors } from 'components/Avatar'; import { ButtonTypes } from 'components/Button/Button.types'; -import List from 'components/List'; +import List, { ListItem, ListItemText } from 'components/List'; export type MenuProps = { /** the color of the button based on our colors eg. red-500 */ @@ -80,13 +76,20 @@ const Menu: React.FC = (props) => {
    {items && ( ({ value: item, label: item }))} - rowSize={'small'} - handleOptionClick={(option) => { + label={'filter-options'} + onSelectionChange={(keys) => { setIsOpen(false); - onSelect(option.value); + const keyFound = String(head(Array.from(keys))); + const optionFound = items.find((o) => o === keyFound); + optionFound && onSelect(optionFound); }} - /> + > + {items.map((item) => ( + + {item} + + ))} + )}
    )} diff --git a/src/components/Select/Select.stories.mdx b/src/components/Select/Select.stories.mdx index babaefc2a..321ea44d5 100644 --- a/src/components/Select/Select.stories.mdx +++ b/src/components/Select/Select.stories.mdx @@ -12,7 +12,9 @@ const options = [ { value: 'chocolate', label: 'Chocolate' }, { value: 'strawberry', label: 'Strawberry' }, { value: 'banana', label: 'Banana' }, - { value: 'citrus', label: 'Citrus' }, + { value: 'citrus2', label: 'Citrus2' }, + { value: 'citrus3', label: 'Citrus3' }, + { value: 'citrus4', label: 'Citrus4' }, { value: 'vanilla', label: 'Vanilla', isDisabled: true }, ]; const optionsWithHelperInDisabled = [ @@ -24,16 +26,16 @@ const optionsWithHelperInDisabled = [ ]; const groupOptions = [ { - value: 'group1', - label: 'Group 1', + value: 'Sweet', + label: 'Sweets', options: [ - { value: '1', label: 'Option 1' }, + { value: '1', label: 'Chocolate' }, { value: '2', label: 'Option 2' }, ], }, { - value: 'group2', - label: 'Group 2', + value: 'Sour', + label: 'Sour', options: [{ value: '3', label: 'Option 3' }], }, { value: '4', label: 'Option 4' }, @@ -79,18 +81,14 @@ const defaultValue = options[0]; -## Overview - -A universal Select component that is a clickable element that can hold many forms. - - [Overview](#overview) - [Props](#props) - [Usage](#usage) - [Variants](#variants) -## Props +## Overview - +A universal Select component that is a clickable element that can hold many forms. ## Usage @@ -103,6 +101,10 @@ A universal Select component that is a clickable element that can hold many form ]} /> +## Props + + + ## Variants ### Simple Select @@ -122,6 +124,7 @@ Simple select component with clearable data and searchable input selectedOption={selectedOption} onChange={setSelectedOption} isCreatable={boolean('isCreatable', false)} + isVirtualized={boolean('isVirtualized', true)} /> ); }} diff --git a/src/components/Select/Select.test.tsx b/src/components/Select/Select.test.tsx index 1983ce9c3..4827262a2 100644 --- a/src/components/Select/Select.test.tsx +++ b/src/components/Select/Select.test.tsx @@ -4,6 +4,7 @@ import React from 'react'; import { render, screen, selectDropdownOption, waitFor } from '../../test'; import StatefulSelect from './StatefulSelect'; import { fireEvent } from '@testing-library/react'; +import { SELECT_ALL_OPTION } from './constants'; global.ResizeObserver = jest.fn().mockImplementation(() => ({ observe: jest.fn(), @@ -58,8 +59,6 @@ describe('Generic Select', () => { selectInput = screen.getByPlaceholderText('Country') as HTMLInputElement; await selectDropdownOption(selectInput, dropdownList[1].label); - screen.debug(); - debugger; expect(handleSubmit).toHaveBeenCalledTimes(1); }); @@ -365,7 +364,7 @@ describe('Multi Select', () => { it('selects all options when Select All is clicked', async () => { userEvent.click(selectInput); - userEvent.click(screen.getByTestId('ictinus_list_default_option')); + userEvent.click(screen.getByTestId(`ictinus_list_item_${SELECT_ALL_OPTION.value}`)); chips = await screen.findAllByTestId(/chip-chip_/); diff --git a/src/components/Select/Select.tsx b/src/components/Select/Select.tsx index a75d057e0..4016bf3eb 100644 --- a/src/components/Select/Select.tsx +++ b/src/components/Select/Select.tsx @@ -1,5 +1,5 @@ import useKeyboard from 'hooks/useKeyboarEvents'; -import { differenceBy } from 'lodash'; +import { differenceBy, head } from 'lodash'; import debounce from 'lodash/debounce'; import React, { useCallback, useMemo, useRef, useState } from 'react'; import isEqual from 'react-fast-compare'; @@ -51,9 +51,12 @@ const Select = React.forwardRef((props, ref) => { setIsOpen(true); // set on diff thread to wait to open setTimeout(() => { - const firstChild = listRef.current?.firstChild; - if (firstChild instanceof HTMLElement && typeof firstChild.focus === 'function') { - firstChild.focus(); + const options = listRef.current?.querySelectorAll('[role="option"]'); + if (options && options?.length > 0) { + const firstOption = head(options); + if (firstOption instanceof HTMLElement && typeof firstOption.focus === 'function') { + firstOption.focus(); + } } }, 0); }, diff --git a/src/components/Select/components/SelectMenu/SelectMenu.tsx b/src/components/Select/components/SelectMenu/SelectMenu.tsx index 1cc2b4b31..2afe1f562 100644 --- a/src/components/Select/components/SelectMenu/SelectMenu.tsx +++ b/src/components/Select/components/SelectMenu/SelectMenu.tsx @@ -1,10 +1,12 @@ import useCombinedRefs from 'hooks/useCombinedRefs'; +import { flatMap, head } from 'lodash'; +import uniqueId from 'lodash/uniqueId'; import React, { forwardRef, useEffect, useRef } from 'react'; import { menuStyle, optionStyle } from './SelectMenu.style'; import { SelectOption } from '../../types'; -import List from 'components/List'; -import { MAX_NON_VIRTUALIZED_ITEMS_SELECT } from 'components/List/utils'; +import List, { ListItem, ListItemText, ListSection } from 'components/List'; +import { COMPACT_LIST_ITEM_HEIGHT, MAX_NON_VIRTUALIZED_ITEMS_SELECT } from 'components/List/utils'; import { SELECT_ALL_OPTION } from 'components/Select/constants'; import { TextInputBaseProps } from 'components/TextInputBase'; @@ -30,8 +32,11 @@ const SelectMenu = forwardRef((props, ref) => } = props; const myRef = useRef(null); const combinedRefs = useCombinedRefs(myRef, ref); + const minListHeightWithCompactListItem = 5 * COMPACT_LIST_ITEM_HEIGHT; // 40 is the height of compact list item and we want to show 5 on render - const executeScroll = () => myRef.current?.scrollIntoView({ block: 'nearest', inline: 'start' }); + const executeScroll = () => + myRef.current?.scrollIntoView && + myRef.current?.scrollIntoView({ block: 'nearest', inline: 'start' }); useEffect(() => { executeScroll(); @@ -40,15 +45,54 @@ const SelectMenu = forwardRef((props, ref) => const renderOptions = () => filteredOptions.length > 0 ? ( MAX_NON_VIRTUALIZED_ITEMS_SELECT} - handleOptionClick={handleOptionClick} - searchTerm={searchTerm} - selectedItem={selectedOption} - defaultOption={hasSelectAllOption ? SELECT_ALL_OPTION : undefined} - /> + onSelectionChange={(keys) => { + const keyFound = String(head(Array.from(keys))); + if (keyFound === SELECT_ALL_OPTION.value) { + handleOptionClick(SELECT_ALL_OPTION); + } else { + const optionFound = flatMap(filteredOptions, (o) => o.options || o).find( + (o) => String(o.value) === keyFound + ); + optionFound && handleOptionClick(optionFound); + } + }} + // searchTerm={searchTerm} + selectedKeys={[selectedOption.value]} + disabledKeys={filteredOptions.filter((o) => o.isDisabled).map((o) => o.value)} + > + {hasSelectAllOption ? ( + + {SELECT_ALL_OPTION.label} + + ) : null} + {filteredOptions.map((option) => { + if (option.options && option.options?.length > 0) { + return ( + + {option.options.map((o) => ( + + {o.label} + + ))} + + ); + } + + return ( + + {option.label} + + ); + })} + ) : (
    No options
    ); diff --git a/src/components/Select/constants.ts b/src/components/Select/constants.ts index 9a4dc033e..6e8b064e9 100644 --- a/src/components/Select/constants.ts +++ b/src/components/Select/constants.ts @@ -1 +1,6 @@ -export const SELECT_ALL_OPTION = { value: 'select_all', label: 'Select All' } as const; +import { SelectOption } from './types'; + +export const SELECT_ALL_OPTION: SelectOption = { + value: 'select_all', + label: 'Select All', +} as const; diff --git a/src/components/storyUtils/ListShowcase.tsx b/src/components/storyUtils/ListShowcase.tsx deleted file mode 100644 index b0da01056..000000000 --- a/src/components/storyUtils/ListShowcase.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; - -import List from '../List'; -import { ListProps } from '../List'; -import { SelectOption } from '../Select'; - -interface Props extends Omit { - itemsCount: number; - isListGroup?: boolean; -} - -const ListShowcase: React.FC = ({ - itemsCount, - isListGroup, - rowSize, - width, - height, - isVirtualized, -}) => { - const items: SelectOption[] = Array(itemsCount) - .fill({}) - .map((__, index) => { - return { - value: index, - label: (isListGroup ? 'Group ' : 'Item ') + index, - options: isListGroup - ? [ - { value: index, label: 'Option 1 of Group ' + index }, - { value: index, label: 'Option 2 of Group ' + index }, - ] - : undefined, - }; - }); - - return ( - - ); -}; - -export default ListShowcase; diff --git a/src/hooks/useElementSize.ts b/src/hooks/useElementSize.ts new file mode 100644 index 000000000..1ccc38435 --- /dev/null +++ b/src/hooks/useElementSize.ts @@ -0,0 +1,47 @@ +import { useCallback, useState } from 'react'; + +// See: https://usehooks-ts.com/react-hook/use-event-listener +import useEventListener from './useEventListener'; +// See: https://usehooks-ts.com/react-hook/use-isomorphic-layout-effect +import useIsomorphicLayoutEffect from './useIsoMorphicLayoutEffect'; + +interface Size { + width: number; + height: number; +} + +function useElementSize(): [ + (node: T | null) => void, + Size, + T | null +] { + // Mutable values like 'ref.current' aren't valid dependencies + // because mutating them doesn't re-render the component. + // Instead, we use a state as a ref to be reactive. + const [ref, setRef] = useState(null); + const [size, setSize] = useState({ + width: 0, + height: 0, + }); + + // Prevent too many rendering using useCallback + const handleSize = useCallback(() => { + setSize({ + width: ref?.offsetWidth || 0, + height: ref?.offsetHeight || 0, + }); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ref?.offsetHeight, ref?.offsetWidth]); + + useEventListener('resize', handleSize); + + useIsomorphicLayoutEffect(() => { + handleSize(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ref?.offsetHeight, ref?.offsetWidth]); + + return [setRef, size, ref]; +} + +export default useElementSize; diff --git a/src/hooks/useEventListener.ts b/src/hooks/useEventListener.ts new file mode 100644 index 000000000..04822523f --- /dev/null +++ b/src/hooks/useEventListener.ts @@ -0,0 +1,42 @@ +import { RefObject, useEffect, useRef } from 'react'; + +import useIsomorphicLayoutEffect from './useIsoMorphicLayoutEffect'; + +// See: https://usehooks-ts.com/react-hook/use-isomorphic-layout-effect + +function useEventListener< + KW extends keyof WindowEventMap, + KH extends keyof HTMLElementEventMap, + T extends HTMLElement | void = void +>( + eventName: KW | KH, + handler: (event: WindowEventMap[KW] | HTMLElementEventMap[KH] | Event) => void, + element?: RefObject +) { + // Create a ref that stores handler + const savedHandler = useRef(handler); + + useIsomorphicLayoutEffect(() => { + savedHandler.current = handler; + }, [handler]); + + useEffect(() => { + // Define the listening target + const targetElement: T | Window = element?.current || window; + if (!(targetElement && targetElement.addEventListener)) { + return; + } + + // Create event listener that calls handler function stored in ref + const eventListener: typeof handler = (event) => savedHandler.current(event); + + targetElement.addEventListener(eventName, eventListener); + + // Remove event listener on cleanup + return () => { + targetElement.removeEventListener(eventName, eventListener); + }; + }, [eventName, element]); +} + +export default useEventListener; diff --git a/src/hooks/useIsoMorphicLayoutEffect.ts b/src/hooks/useIsoMorphicLayoutEffect.ts new file mode 100644 index 000000000..b285db784 --- /dev/null +++ b/src/hooks/useIsoMorphicLayoutEffect.ts @@ -0,0 +1,5 @@ +import { useEffect, useLayoutEffect } from 'react'; + +const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect; + +export default useIsomorphicLayoutEffect; diff --git a/src/storybook.test.ts b/src/storybook.test.ts index 93cb9a8f2..f0877020d 100644 --- a/src/storybook.test.ts +++ b/src/storybook.test.ts @@ -38,7 +38,11 @@ function createNodeMock(story: Story) { return htmlDivElementRefMock; } - if (story.name === 'List') { + /** React-Aria bypass with the extra props needed **/ + if ( + element.props?.role === 'listbox' || + element.props['data-testid']?.includes('ictinus_list') + ) { return { ...element, setProps: () => {}, diff --git a/src/test/setup.ts b/src/test/setup.ts index b2402b6cf..dad910b73 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -47,3 +47,8 @@ Object.defineProperty(window, 'matchMedia', { dispatchEvent: jest.fn(), })), }); + +jest.mock('@react-aria/ssr/dist/main', () => ({ + ...jest.requireActual('@react-aria/ssr/dist/main'), + useSSRSafeId: () => 'react-aria-generated-id', +})); diff --git a/yarn.lock b/yarn.lock index edeb6c27f..fbeeda759 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14044,16 +14044,16 @@ memfs@^3.2.2: dependencies: fs-monkey "1.0.3" -"memoize-one@>=3.1.1 <6", memoize-one@^5.0.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" - integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== - memoize-one@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.1.0.tgz#a2387c58c03fff27ca390c31b764a79addf3f906" integrity sha512-2GApq0yI/b22J2j9rhbrAlsHb0Qcz+7yWxeLG8h+95sl1XPUgeLimQSOdur4Vw7cUhrBHwaUZxWFZueojqNRzA== +memoize-one@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + memoizerific@^1.11.3: version "1.11.3" resolved "https://registry.yarnpkg.com/memoizerific/-/memoizerific-1.11.3.tgz#7c87a4646444c32d75438570905f2dbd1b1a805a" @@ -16458,14 +16458,6 @@ react-transition-group@^4.3.0: loose-envify "^1.4.0" prop-types "^15.6.2" -react-window@^1.8.6: - version "1.8.6" - resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.6.tgz#d011950ac643a994118632665aad0c6382e2a112" - integrity sha512-8VwEEYyjz6DCnGBsd+MgkD0KJ2/OXFULyDtorIiTz+QzwoP94tBoA7CnbtyXMm+cCeAUER5KJcPtWl9cpKbOBg== - dependencies: - "@babel/runtime" "^7.0.0" - memoize-one ">=3.1.1 <6" - react@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"