From c61220b6ee9487fa01be8cba39de99619d094bbf Mon Sep 17 00:00:00 2001 From: panagiotis vourtsis Date: Fri, 21 Jul 2023 14:49:59 +0300 Subject: [PATCH 01/15] feat(list): change aria list to barebone aria --- package.json | 1 - .../DropdownButton/DropdownButton.test.tsx | 2 +- src/components/List/List.stories.mdx | 25 +- src/components/List/List.style.ts | 1 + src/components/List/List.tsx | 87 +- src/components/List/ListBox.tsx | 118 + .../List/ListItem/ListItem.style.ts | 42 +- src/components/List/ListItem/ListItem.tsx | 46 +- .../ListGroupTitle/ListGroupTitle.style.ts | 31 - .../ListGroupTitle/ListGroupTitle.tsx | 38 - .../ListItemGroup/ListGroupTitle/index.ts | 2 - .../List/ListItemGroup/ListItemGroup.tsx | 62 - src/components/List/ListItemGroup/index.ts | 2 - .../List/NormalList/NormalList.style.ts | 1 - src/components/List/NormalList/NormalList.tsx | 119 - src/components/List/NormalList/index.ts | 2 - .../VirtualizedList/VirtualizedList.style.ts | 0 .../List/VirtualizedList/VirtualizedList.tsx | 114 - src/components/List/VirtualizedList/index.ts | 2 - src/components/List/Window.tsx | 93 + .../List/__snapshots__/List.stories.storyshot | 2028 ++++++++++++++--- src/components/List/utils.tsx | 24 +- src/components/Select/Select.stories.mdx | 5 +- src/components/Select/Select.test.tsx | 2 + .../components/SelectMenu/SelectMenu.tsx | 4 +- src/components/Select/constants.ts | 8 +- src/components/Select/types.ts | 1 + src/components/storyUtils/ListShowcase.tsx | 5 +- src/hooks/useElementSize.ts | 47 + src/hooks/useEventListener.ts | 42 + src/hooks/useIsoMorphicLayoutEffect.ts | 5 + src/test/setup.ts | 5 + 32 files changed, 2232 insertions(+), 732 deletions(-) create mode 100644 src/components/List/ListBox.tsx delete mode 100644 src/components/List/ListItemGroup/ListGroupTitle/ListGroupTitle.style.ts delete mode 100644 src/components/List/ListItemGroup/ListGroupTitle/ListGroupTitle.tsx delete mode 100644 src/components/List/ListItemGroup/ListGroupTitle/index.ts delete mode 100644 src/components/List/ListItemGroup/ListItemGroup.tsx delete mode 100644 src/components/List/ListItemGroup/index.ts delete mode 100644 src/components/List/NormalList/NormalList.style.ts delete mode 100644 src/components/List/NormalList/NormalList.tsx delete mode 100644 src/components/List/NormalList/index.ts delete mode 100644 src/components/List/VirtualizedList/VirtualizedList.style.ts delete mode 100644 src/components/List/VirtualizedList/VirtualizedList.tsx delete mode 100644 src/components/List/VirtualizedList/index.ts create mode 100644 src/components/List/Window.tsx create mode 100644 src/hooks/useElementSize.ts create mode 100644 src/hooks/useEventListener.ts create mode 100644 src/hooks/useIsoMorphicLayoutEffect.ts diff --git a/package.json b/package.json index 06d57f61b..7f913875a 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,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/List/List.stories.mdx b/src/components/List/List.stories.mdx index 6626726a8..98921e5ae 100644 --- a/src/components/List/List.stories.mdx +++ b/src/components/List/List.stories.mdx @@ -41,16 +41,29 @@ List component. +### Group List + +Group List component. + + + + + + + ### Virtualized List with knobs Virtualized List component for large amounts of items. @@ -58,7 +71,7 @@ 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..311cad08b 100644 --- a/src/components/List/List.style.ts +++ b/src/components/List/List.style.ts @@ -33,6 +33,7 @@ export const listStyle = height: ${height ? rem(height) : '100%'}; overflow: auto; overflow-x: hidden; + background: #fff; `; export const listLabelHelperText = (theme: Theme): SerializedStyles => css` diff --git a/src/components/List/List.tsx b/src/components/List/List.tsx index 62c250465..a29b978e9 100644 --- a/src/components/List/List.tsx +++ b/src/components/List/List.tsx @@ -1,11 +1,12 @@ +import { Item, Section } from '@react-stately/collections'; import React, { memo } from 'react'; import isEqual from 'react-fast-compare'; import { TestProps } from 'utils/types'; -import { wrapperStyle } from './List.style'; -import NormalList from './NormalList'; +import { listStyle, wrapperStyle } from './List.style'; +import { ListBox } from './ListBox'; import { ListItemType, ListRowSize, SelectHandlerType } from './types'; -import VirtualizedList from './VirtualizedList'; +import { SelectOption } from '../Select'; export type ListProps = { /** Data for the list */ @@ -14,7 +15,7 @@ export type ListProps = { rowSize: ListRowSize; /** 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; @@ -49,38 +50,60 @@ const List = React.forwardRef( }, ref ) => { + const newItems = defaultOption ? [{ ...defaultOption, isDefaultOption: true }, ...data] : data; + + const selectedOption = newItems.find((item) => item.value === selectedItem?.value); + return (
- {isVirtualized ? ( - - ) : ( - + } + selectionMode="single" + items={newItems.map((item) => ({ ...item, id: item.value }))} + // @ts-ignore // hack to work skip the logic + isVirtualized={true} + isVirtualizationEnabled={isVirtualized} + css={listStyle({ width, height, isSearchable })} height={height} - ref={ref} - selectedItem={selectedItem} - isSearchable={isSearchable} - defaultOption={defaultOption} - searchTerm={searchTerm} - handleOptionClick={handleOptionClick} - dataTestId={dataTestId} {...rest} - /> - )} + selectedKeys={ + selectedOption !== undefined && selectedOption !== null + ? new Set([selectedOption.value]) + : undefined + } + disabledKeys={ + new Set(data.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'} + > + {/* This has to be part of Aria collection. That is why we use explicit Item component from Aria*/} + {(item: SelectOption) => { + return item.options && item.options.length > 0 ? ( +
+ {/*{(item: SelectOption) => {item.label}}*/} + {item.options.map((sectionItem) => ( + {sectionItem.label} + ))} +
+ ) : ( + {item.label} + ); + }} +
+
); } diff --git a/src/components/List/ListBox.tsx b/src/components/List/ListBox.tsx new file mode 100644 index 000000000..f487a8166 --- /dev/null +++ b/src/components/List/ListBox.tsx @@ -0,0 +1,118 @@ +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 * as React from 'react'; +import { forwardRef } from 'react'; +import { AriaListBoxProps, useListBoxSection } from 'react-aria'; +// import { VariableSizeList as VList } from 'react-window'; + +import ListItem from './ListItem'; +import { listItemStyle } from './ListItem/ListItem.style'; +import Window from './Window'; +import useCombinedRefs from '../../hooks/useCombinedRefs'; +import { SelectOption } from '../Select'; + +export const ListBox = forwardRef< + HTMLUListElement, + AriaListBoxProps & { height?: number; isVirtualizationEnabled?: boolean } +>((props, ref) => { + // Create state based on the incoming props + const state = useListState(props); + const myRef = React.useRef(null); + const combinedRefs = useCombinedRefs(myRef, ref); + + const { listBoxProps } = useListBox(props, state, combinedRefs); + + return ( +
+ + {[...state.collection].map((item) => { + const options = item?.value?.options; + + return options && options.length > 0 ? ( + + ) : ( + +
+ ); +}); +ListBox.displayName = 'ListBox'; + +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 ( + + ); +} + +function ListBoxSection({ section, state }: any) { + const { itemProps, headingProps, groupProps } = useListBoxSection({ + heading: section.rendered, + 'aria-label': section['aria-label'], + }); + + // If the section is not the first, add a separator element to provide visual separation. + // The heading is rendered inside an
  • element, which contains + // a
      with the child items. + return ( + <> +
    • + {section.rendered && ( + + {section.rendered} + + )} +
        + {[...section.childNodes].map((node) => ( +
      +
    • + + ); +} diff --git a/src/components/List/ListItem/ListItem.style.ts b/src/components/List/ListItem/ListItem.style.ts index 125b8512d..b2621578c 100644 --- a/src/components/List/ListItem/ListItem.style.ts +++ b/src/components/List/ListItem/ListItem.style.ts @@ -6,28 +6,42 @@ import { ListRowSize } from '../types'; export const listItemStyle = ({ - size, + isGroupItem = false, isHighlighted, isDisabled, - isGroupItem, }: { - size: ListRowSize; + isGroupItem?: boolean; 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')}; + (theme: Theme): SerializedStyles => { + const padding = css`0 ${theme.globals.spacing.get('6')} 0px ${theme.globals.spacing.get('6')}`; + const itemHeight = rem(56); + + return css` + min-height: ${isGroupItem ? undefined : itemHeight}; + color: ${theme.tokens.textColor.get('light.primary')}; + font-size: ${theme.globals.typography.fontSize.get('4')}; 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; + flex-direction: column; + //align-items: center; + padding: ${isGroupItem ? undefined : padding}; + font-weight: ${isGroupItem ? 'bold' : 'initial'}; ${isHighlighted && 'font-weight: 500;'} + &[role='option'] { + cursor: pointer; + } + + > span { + align-items: center; + display: flex; + // move styling inside span which is the header when section + min-height: ${itemHeight}; + padding: ${padding}; + color: ${theme.tokens.textColor.get('light.secondary')}; + } &[data-focus-visible] { background-color: ${theme.utils.getColor('lightGrey', 50)}; @@ -51,7 +65,7 @@ export const listItemStyle = font-weight: bold; } - &:hover { + &[role='option']:hover { background-color: ${theme.utils.getColor('lightGrey', 50)}; } @@ -61,6 +75,7 @@ export const listItemStyle = cursor: not-allowed; `} `; + }; export const contentStyle = (): SerializedStyles => css` white-space: nowrap; @@ -70,6 +85,7 @@ export const contentStyle = (): SerializedStyles => css` flex: 1; flex-direction: row; display: flex; + align-items: center; > div { flex: 1; } diff --git a/src/components/List/ListItem/ListItem.tsx b/src/components/List/ListItem/ListItem.tsx index 0ee23a225..2707895de 100644 --- a/src/components/List/ListItem/ListItem.tsx +++ b/src/components/List/ListItem/ListItem.tsx @@ -4,15 +4,11 @@ import { TestProps } from 'utils/types'; import { listItemStyle, contentStyle } from './ListItem.style'; import { ListItemType, ListRowSize, SelectHandlerType } from '../types'; -import { renderContent } from '../utils'; +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 */ @@ -21,45 +17,31 @@ export type ListItemProps = { 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 */ + /** Determines if the item is a header of a section */ isGroupItem?: boolean; } & TestProps & Omit, 'value'>; -const ListItem = React.forwardRef( +const ListItem = React.forwardRef( ( - { - size, - content, - index, - isSelected = false, - isHighlighted = false, - isDisabled = false, - handleOptionClick, - searchTerm, - dataTestId, - isGroupItem, - ...rest - }, + { content, isDisabled = false, isHighlighted = false, searchTerm, dataTestId, ...rest }, ref ) => { return ( -
      - { - /** @TODO latest version typescript 4.4 is solving this as a constant */ - renderContent(content, searchTerm) - } +
      -
      + ); } ); 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/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..f683d2d56 --- /dev/null +++ b/src/components/List/Window.tsx @@ -0,0 +1,93 @@ +import { mergeProps } from '@react-aria/utils'; +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; + +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, { + // style: { + // position: 'absolute', + // top: index * rowHeight + index * gap, + // height: rowHeight, + // left: 0, + // right: 0, + // lineHeight: `${rowHeight}px`, + // }, + }) + ); + } + 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, + { 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 index d7b9f81be..589919ce8 100644 --- a/src/components/List/__snapshots__/List.stories.storyshot +++ b/src/components/List/__snapshots__/List.stories.storyshot @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Storyshots Design System/List List 1`] = ` +exports[`Storyshots Design System/List Group List 1`] = ` .emotion-0 { -webkit-flex-direction: column; -ms-flex-direction: column; @@ -19,18 +19,156 @@ exports[`Storyshots Design System/List List 1`] = ` .emotion-2 { border: 1px solid #e7ebf2; border-radius: 0.25rem; - width: 650px; + width: undefinedpx; } .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; + min-height: 3.5rem; + color: #212332; + font-size: 1rem; + background-color: white; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + padding: 0 1rem 0px 1rem; + font-weight: initial; +} + +.emotion-3[role='option'] { + cursor: pointer; +} + +.emotion-3>span { + -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; + min-height: 3.5rem; + padding: 0 1rem 0px 1rem; + color: #54587f; +} + +.emotion-3[data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-3[aria-selected='true'] { + background-color: #e7eefe; + color: #1451dc; + font-weight: 500; +} + +.emotion-3[aria-selected='true'][data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-3[aria-disabled] { + color: var(--text-color-disabled); +} + +.emotion-3 strong { + font-weight: bold; +} + +.emotion-3[role='option']:hover { + background-color: #f3f5f8; +} + +.emotion-4 { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: inherit; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + 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; +} + +.emotion-4>div { + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; +} + +.emotion-5 { + cursor: inherit; +} + +.emotion-6 { + color: #212332; + font-size: 1rem; + background-color: white; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + font-weight: bold; +} + +.emotion-6[role='option'] { + cursor: pointer; +} + +.emotion-6>span { + -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; + min-height: 3.5rem; + padding: 0 1rem 0px 1rem; + color: #54587f; +} + +.emotion-6[data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-6[aria-selected='true'] { + background-color: #e7eefe; + color: #1451dc; + font-weight: 500; +} + +.emotion-6[aria-selected='true'][data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-6[aria-disabled] { + color: var(--text-color-disabled); +} + +.emotion-6 strong { + font-weight: bold; +} + +.emotion-6[role='option']:hover { + background-color: #f3f5f8; }
      - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      `; -exports[`Storyshots Design System/List List with icons 1`] = ` +exports[`Storyshots Design System/List List 1`] = ` .emotion-0 { -webkit-flex-direction: column; -ms-flex-direction: column; @@ -144,59 +742,503 @@ exports[`Storyshots Design System/List List with icons 1`] = ` .emotion-2 { border: 1px solid #e7ebf2; border-radius: 0.25rem; - width: 650px; + width: undefinedpx; } .emotion-3 { - overflow-x: hidden; + min-height: 3.5rem; + color: #212332; + font-size: 1rem; + background-color: white; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + padding: 0 1rem 0px 1rem; + font-weight: initial; +} + +.emotion-3[role='option'] { + cursor: pointer; +} + +.emotion-3>span { + -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; + min-height: 3.5rem; + padding: 0 1rem 0px 1rem; + color: #54587f; +} + +.emotion-3[data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-3[aria-selected='true'] { + background-color: #e7eefe; + color: #1451dc; + font-weight: 500; +} + +.emotion-3[aria-selected='true'][data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-3[aria-disabled] { + color: var(--text-color-disabled); +} + +.emotion-3 strong { + font-weight: bold; +} + +.emotion-3[role='option']:hover { + background-color: #f3f5f8; } .emotion-4 { - position: absolute; - left: 0; - top: 0; - height: 56px; - width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: inherit; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + 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; +} + +.emotion-4>div { + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; } .emotion-5 { - position: absolute; - left: 0; - top: 56px; - height: 56px; - width: 100%; + cursor: inherit; } -.emotion-6 { - position: absolute; - left: 0; - top: 112px; - height: 56px; - width: 100%; +
      +
      +
      +
      +
      +
        +
      • +
        +
        + All +
        +
        +
      • +
      • +
        +
        + Item 0 +
        +
        +
      • +
      • +
        +
        + Item 1 +
        +
        +
      • +
      • +
        +
        + Item 2 +
        +
        +
      • +
      • +
        +
        + Item 3 +
        +
        +
      • +
      • +
        +
        + Item 4 +
        +
        +
      • +
      +
      +
      +
      +
      +
      +`; + +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: undefinedpx; +} + +.emotion-3 { + min-height: 3.5rem; + color: #212332; + font-size: 1rem; + background-color: white; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + padding: 0 1rem 0px 1rem; + font-weight: initial; +} + +.emotion-3[role='option'] { + cursor: pointer; +} + +.emotion-3>span { + -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; + min-height: 3.5rem; + padding: 0 1rem 0px 1rem; + color: #54587f; +} + +.emotion-3[data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-3[aria-selected='true'] { + background-color: #e7eefe; + color: #1451dc; + font-weight: 500; +} + +.emotion-3[aria-selected='true'][data-focus-visible] { + background-color: #f3f5f8; +} + +.emotion-3[aria-disabled] { + color: var(--text-color-disabled); +} + +.emotion-3 strong { + font-weight: bold; +} + +.emotion-3[role='option']:hover { + background-color: #f3f5f8; +} + +.emotion-4 { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: inherit; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + 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; } -.emotion-7 { - position: absolute; - left: 0; - top: 168px; - height: 56px; - width: 100%; +.emotion-4>div { + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; } -.emotion-8 { - position: absolute; - left: 0; - top: 224px; - height: 56px; - width: 100%; +.emotion-5 { + cursor: inherit; } .emotion-9 { - position: absolute; - left: 0; - top: 280px; - height: 56px; - width: 100%; + 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-10 { + fill: #5f6c85; + width: 1rem; + height: 1rem; +} + +.emotion-10 path { + fill: #5f6c85; }
      - - - - - - - - - - - - - - - - - - +
    • +
      +
      + All +
      +
      +
    • +
    • +
      +
      + Option 1 +
      + + + +
      +
    • +
    • +
      +
      + Option 2 +
      + + + +
      +
    • +
    • +
      +
      + Option 3 +
      + + + +
      +
    • +
    • +
      +
      + Option 4 +
      + + + +
      +
    • +
    • +
      +
      + Option 5 +
      + + + +
      +
    • +
    @@ -292,63 +1570,94 @@ exports[`Storyshots Design System/List Virtualized List 1`] = ` } .emotion-3 { - overflow-x: hidden; + min-height: 3.5rem; + color: #212332; + font-size: 1rem; + background-color: white; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + padding: 0 1rem 0px 1rem; + font-weight: initial; } -.emotion-4 { - position: absolute; - left: 0; - top: 0; - height: 56px; - width: 100%; +.emotion-3[role='option'] { + cursor: pointer; } -.emotion-5 { - position: absolute; - left: 0; - top: 56px; - height: 56px; - width: 100%; +.emotion-3>span { + -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; + min-height: 3.5rem; + padding: 0 1rem 0px 1rem; + color: #54587f; } -.emotion-6 { - position: absolute; - left: 0; - top: 112px; - height: 56px; - width: 100%; +.emotion-3[data-focus-visible] { + background-color: #f3f5f8; } -.emotion-7 { - position: absolute; - left: 0; - top: 168px; - height: 56px; - width: 100%; +.emotion-3[aria-selected='true'] { + background-color: #e7eefe; + color: #1451dc; + font-weight: 500; } -.emotion-8 { - position: absolute; - left: 0; - top: 224px; - height: 56px; - width: 100%; +.emotion-3[aria-selected='true'][data-focus-visible] { + background-color: #f3f5f8; } -.emotion-9 { - position: absolute; - left: 0; - top: 280px; - height: 56px; - width: 100%; +.emotion-3[aria-disabled] { + color: var(--text-color-disabled); } -.emotion-10 { - position: absolute; - left: 0; - top: 336px; - height: 56px; - width: 100%; +.emotion-3 strong { + font-weight: bold; +} + +.emotion-3[role='option']:hover { + background-color: #f3f5f8; +} + +.emotion-4 { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + cursor: inherit; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + 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; +} + +.emotion-4>div { + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; +} + +.emotion-5 { + cursor: inherit; }
    - - - - - - - - - - - - - - - - - - - - - +
  • +
    +
    + All +
    +
    +
  • +
  • +
    +
    + Item 0 +
    +
    +
  • +
  • +
    +
    + Item 1 +
    +
    +
  • +
  • +
    +
    + Item 2 +
    +
    +
  • +
  • +
    +
    + Item 3 +
    +
    +
  • + diff --git a/src/components/List/utils.tsx b/src/components/List/utils.tsx index b72fc879f..f2935aaca 100644 --- a/src/components/List/utils.tsx +++ b/src/components/List/utils.tsx @@ -47,7 +47,13 @@ const renderLabelWithHelperText = (content: SelectOption | FilterOption) => { return content.label; }; -export const renderContent = (content: ListItemType, searchTerm?: string) => { +export const RenderContent = ({ + content, + searchTerm, +}: { + 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; + return ( + <> +
    {renderLabelWithHelperText(content)}
    + {content?.iconProps && } + + ); }; diff --git a/src/components/Select/Select.stories.mdx b/src/components/Select/Select.stories.mdx index babaefc2a..9292ff124 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 = [ @@ -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..51b6a7798 100644 --- a/src/components/Select/Select.test.tsx +++ b/src/components/Select/Select.test.tsx @@ -367,6 +367,8 @@ describe('Multi Select', () => { userEvent.click(selectInput); userEvent.click(screen.getByTestId('ictinus_list_default_option')); + screen.debug(); + chips = await screen.findAllByTestId(/chip-chip_/); expect(chips.length).toEqual(dropdownList.length); diff --git a/src/components/Select/components/SelectMenu/SelectMenu.tsx b/src/components/Select/components/SelectMenu/SelectMenu.tsx index 1cc2b4b31..44b9b60a7 100644 --- a/src/components/Select/components/SelectMenu/SelectMenu.tsx +++ b/src/components/Select/components/SelectMenu/SelectMenu.tsx @@ -31,7 +31,9 @@ const SelectMenu = forwardRef((props, ref) => const myRef = useRef(null); const combinedRefs = useCombinedRefs(myRef, ref); - const executeScroll = () => myRef.current?.scrollIntoView({ block: 'nearest', inline: 'start' }); + const executeScroll = () => + myRef.current?.scrollIntoView && + myRef.current?.scrollIntoView({ block: 'nearest', inline: 'start' }); useEffect(() => { executeScroll(); diff --git a/src/components/Select/constants.ts b/src/components/Select/constants.ts index 9a4dc033e..e2b06b81f 100644 --- a/src/components/Select/constants.ts +++ b/src/components/Select/constants.ts @@ -1 +1,7 @@ -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', + isDefaultOption: true, +} as const; diff --git a/src/components/Select/types.ts b/src/components/Select/types.ts index 1d44d8991..7d1c9c488 100644 --- a/src/components/Select/types.ts +++ b/src/components/Select/types.ts @@ -17,6 +17,7 @@ export type SelectOptionBase = { tooltipInfo?: string; options?: SelectOption[]; isCreated?: boolean; + isDefaultOption?: boolean; }; export type SelectOption = SelectOptionBase & SelectOptionValues; diff --git a/src/components/storyUtils/ListShowcase.tsx b/src/components/storyUtils/ListShowcase.tsx index b0da01056..917bbe6ad 100644 --- a/src/components/storyUtils/ListShowcase.tsx +++ b/src/components/storyUtils/ListShowcase.tsx @@ -1,3 +1,4 @@ +import uniqueId from 'lodash/uniqueId'; import React from 'react'; import List from '../List'; @@ -25,8 +26,8 @@ const ListShowcase: React.FC = ({ label: (isListGroup ? 'Group ' : 'Item ') + index, options: isListGroup ? [ - { value: index, label: 'Option 1 of Group ' + index }, - { value: index, label: 'Option 2 of Group ' + index }, + { value: uniqueId(), label: 'Option 1 of Group ' + index }, + { value: uniqueId(), label: 'Option 2 of Group ' + index }, ] : undefined, }; 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/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', +})); From 1f2bba074b0c9b1d745c9087f92db05b15a4f2d8 Mon Sep 17 00:00:00 2001 From: panagiotis vourtsis Date: Wed, 26 Jul 2023 20:30:24 +0300 Subject: [PATCH 02/15] feat(list): change list to separate components --- package.json | 1 - .../DropdownButton/DropdownButton.tsx | 24 +- .../Filter/components/Options/Options.tsx | 39 ++- src/components/List/List.stories.mdx | 122 ---------- src/components/List/List.style.ts | 2 +- src/components/List/List.tsx | 225 +++++++++++------- src/components/List/ListBox.tsx | 118 --------- src/components/List/ListItem.tsx | 23 ++ .../List/ListItem/ListItem.style.ts | 92 ------- src/components/List/ListItem/ListItem.tsx | 50 ---- src/components/List/ListItem/index.ts | 2 - src/components/List/ListSection.tsx | 21 ++ src/components/List/Window.tsx | 20 +- .../ListItemAction/ListItemAction.style.ts | 11 + .../ListItemAction/ListItemAction.tsx | 11 + .../List/components/ListItemAction/index.ts | 2 + .../ListItemText/ListItemText.style.ts | 26 ++ .../components/ListItemText/ListItemText.tsx | 18 ++ .../List/components/ListItemText/index.ts | 2 + .../ListItemWrapper/ListItemWrapper.style.ts | 69 ++++++ .../ListItemWrapper/ListItemWrapper.tsx | 52 ++++ .../List/components/ListItemWrapper/README.md | 1 + .../List/components/ListItemWrapper/index.ts | 2 + src/components/List/index.ts | 4 + src/components/List/stories/List.stories.mdx | 140 +++++++++++ .../List/stories/ListItem.stories.mdx | 145 +++++++++++ .../List/stories/ListItemAction.stories.mdx | 68 ++++++ .../List/stories/ListItemText.stories.mdx | 120 ++++++++++ .../List/stories/ListSection.stories.mdx | 143 +++++++++++ src/components/List/types.ts | 7 +- src/components/Menu/Menu.tsx | 28 ++- src/components/Select/Select.stories.mdx | 22 +- src/components/Select/Select.tsx | 11 +- .../components/SelectMenu/SelectMenu.tsx | 58 ++++- src/components/storyUtils/ListShowcase.tsx | 49 ---- yarn.lock | 18 +- 36 files changed, 1143 insertions(+), 603 deletions(-) delete mode 100644 src/components/List/List.stories.mdx delete mode 100644 src/components/List/ListBox.tsx create mode 100644 src/components/List/ListItem.tsx delete mode 100644 src/components/List/ListItem/ListItem.style.ts delete mode 100644 src/components/List/ListItem/ListItem.tsx delete mode 100644 src/components/List/ListItem/index.ts create mode 100644 src/components/List/ListSection.tsx create mode 100644 src/components/List/components/ListItemAction/ListItemAction.style.ts create mode 100644 src/components/List/components/ListItemAction/ListItemAction.tsx create mode 100644 src/components/List/components/ListItemAction/index.ts create mode 100644 src/components/List/components/ListItemText/ListItemText.style.ts create mode 100644 src/components/List/components/ListItemText/ListItemText.tsx create mode 100644 src/components/List/components/ListItemText/index.ts create mode 100644 src/components/List/components/ListItemWrapper/ListItemWrapper.style.ts create mode 100644 src/components/List/components/ListItemWrapper/ListItemWrapper.tsx create mode 100644 src/components/List/components/ListItemWrapper/README.md create mode 100644 src/components/List/components/ListItemWrapper/index.ts create mode 100644 src/components/List/stories/List.stories.mdx create mode 100644 src/components/List/stories/ListItem.stories.mdx create mode 100644 src/components/List/stories/ListItemAction.stories.mdx create mode 100644 src/components/List/stories/ListItemText.stories.mdx create mode 100644 src/components/List/stories/ListSection.stories.mdx delete mode 100644 src/components/storyUtils/ListShowcase.tsx diff --git a/package.json b/package.json index 7f913875a..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", diff --git a/src/components/DropdownButton/DropdownButton.tsx b/src/components/DropdownButton/DropdownButton.tsx index b1793f455..606e4d160 100644 --- a/src/components/DropdownButton/DropdownButton.tsx +++ b/src/components/DropdownButton/DropdownButton.tsx @@ -13,7 +13,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 } from 'components/List'; import ClickAwayListener from 'components/utils/ClickAwayListener'; import { MenuPositionAllowed, optionsStyle } from 'components/utils/DropdownOptions'; @@ -57,9 +57,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] ); @@ -106,11 +106,21 @@ const DropdownButton = React.forwardRef(
    {items && ( ({ value: item, label: item }))} - rowSize={'small'} - handleOptionClick={handleOptionClick} + label={'dropdown-button'} + onSelectionChange={(keys) => { + setIsOpen(false); + const keyFound = String([...keys][0]); + const optionFound = items.find((o) => o === keyFound); + optionFound && handleOptionClick(optionFound); + }} dataTestId={generateTestDataId('dropdown-button-options', dataTestPrefixId)} - /> + > + {items.map((item) => ( + + {item} + + ))} + )}
    )} diff --git a/src/components/Filter/components/Options/Options.tsx b/src/components/Filter/components/Options/Options.tsx index 49a1b7e16..4b65b6ff0 100644 --- a/src/components/Filter/components/Options/Options.tsx +++ b/src/components/Filter/components/Options/Options.tsx @@ -1,9 +1,11 @@ +import { flatMap } from 'lodash'; import React 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 } from 'components/List'; import { MAX_NON_VIRTUALIZED_ITEMS_FILTER } from 'components/List/utils'; export interface Props { @@ -33,16 +35,37 @@ const Options: React.FC = ({ 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={(keys) => { + const keyFound = String([...keys][0]); + if (keyFound === SELECT_ALL_OPTION.value) { + onSelect(SELECT_ALL_OPTION); + } else { + const optionFound = flatMap(items, (o) => o.options || o).find( + (o) => o.value === keyFound + ); + optionFound && onSelect(optionFound); + } + }} 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 98921e5ae..000000000 --- a/src/components/List/List.stories.mdx +++ /dev/null @@ -1,122 +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. - - - - - - - -### Group List - -Group 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 311cad08b..9f09bd379 100644 --- a/src/components/List/List.style.ts +++ b/src/components/List/List.style.ts @@ -30,7 +30,7 @@ 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: #fff; diff --git a/src/components/List/List.tsx b/src/components/List/List.tsx index a29b978e9..83fd46838 100644 --- a/src/components/List/List.tsx +++ b/src/components/List/List.tsx @@ -1,113 +1,156 @@ -import { Item, Section } from '@react-stately/collections'; -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 ListItemWrapper from './components/ListItemWrapper/ListItemWrapper'; +import { listItemWrapperStyle } from './components/ListItemWrapper/ListItemWrapper.style'; import { listStyle, wrapperStyle } from './List.style'; -import { ListBox } from './ListBox'; -import { ListItemType, ListRowSize, SelectHandlerType } from './types'; +import { ListSelected, ListSelection } from './types'; +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; + label: string; /** Width of the list */ width?: number; /** 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; + // /** 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 - ) => { - const newItems = defaultOption ? [{ ...defaultOption, isDefaultOption: true }, ...data] : data; +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 selectedOption = newItems.find((item) => item.value === selectedItem?.value); - - return ( -
    -
    - } - selectionMode="single" - items={newItems.map((item) => ({ ...item, id: item.value }))} - // @ts-ignore // hack to work skip the logic - isVirtualized={true} + return ( +
    +
    +
    + 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'} + rowHeight={40} + ref={combinedRefs} > - {/* This has to be part of Aria collection. That is why we use explicit Item component from Aria*/} - {(item: SelectOption) => { - return item.options && item.options.length > 0 ? ( -
    - {/*{(item: SelectOption) => {item.label}}*/} - {item.options.map((sectionItem) => ( - {sectionItem.label} - ))} -
    + {[...state.collection].map((item) => { + return item.type === 'section' ? ( + ) : ( - {item.label} +
    - ); - } -); +
    + ); +}); List.displayName = 'List'; -export default memo(List, isEqual); +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} + + ); +} + +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/ListBox.tsx b/src/components/List/ListBox.tsx deleted file mode 100644 index f487a8166..000000000 --- a/src/components/List/ListBox.tsx +++ /dev/null @@ -1,118 +0,0 @@ -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 * as React from 'react'; -import { forwardRef } from 'react'; -import { AriaListBoxProps, useListBoxSection } from 'react-aria'; -// import { VariableSizeList as VList } from 'react-window'; - -import ListItem from './ListItem'; -import { listItemStyle } from './ListItem/ListItem.style'; -import Window from './Window'; -import useCombinedRefs from '../../hooks/useCombinedRefs'; -import { SelectOption } from '../Select'; - -export const ListBox = forwardRef< - HTMLUListElement, - AriaListBoxProps & { height?: number; isVirtualizationEnabled?: boolean } ->((props, ref) => { - // Create state based on the incoming props - const state = useListState(props); - const myRef = React.useRef(null); - const combinedRefs = useCombinedRefs(myRef, ref); - - const { listBoxProps } = useListBox(props, state, combinedRefs); - - return ( -
    - - {[...state.collection].map((item) => { - const options = item?.value?.options; - - return options && options.length > 0 ? ( - - ) : ( - -
    - ); -}); -ListBox.displayName = 'ListBox'; - -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 ( - - ); -} - -function ListBoxSection({ section, state }: any) { - const { itemProps, headingProps, groupProps } = useListBoxSection({ - heading: section.rendered, - 'aria-label': section['aria-label'], - }); - - // If the section is not the first, add a separator element to provide visual separation. - // The heading is rendered inside an
  • element, which contains - // a
      with the child items. - return ( - <> -
    • - {section.rendered && ( - - {section.rendered} - - )} -
        - {[...section.childNodes].map((node) => ( -
      -
    • - - ); -} 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 b2621578c..000000000 --- a/src/components/List/ListItem/ListItem.style.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { css, SerializedStyles } from '@emotion/react'; -import { Theme } from 'theme'; -import { rem } from 'theme/utils'; - -import { ListRowSize } from '../types'; - -export const listItemStyle = - ({ - isGroupItem = false, - isHighlighted, - isDisabled, - }: { - isGroupItem?: boolean; - isHighlighted: boolean; - isDisabled: boolean; - }) => - (theme: Theme): SerializedStyles => { - const padding = css`0 ${theme.globals.spacing.get('6')} 0px ${theme.globals.spacing.get('6')}`; - const itemHeight = rem(56); - - return css` - min-height: ${isGroupItem ? undefined : itemHeight}; - color: ${theme.tokens.textColor.get('light.primary')}; - font-size: ${theme.globals.typography.fontSize.get('4')}; - background-color: ${theme.globals.colors.white}; - display: flex; - flex-direction: column; - //align-items: center; - padding: ${isGroupItem ? undefined : padding}; - font-weight: ${isGroupItem ? 'bold' : 'initial'}; - - ${isHighlighted && 'font-weight: 500;'} - &[role='option'] { - cursor: pointer; - } - - > span { - align-items: center; - display: flex; - // move styling inside span which is the header when section - min-height: ${itemHeight}; - padding: ${padding}; - color: ${theme.tokens.textColor.get('light.secondary')}; - } - - &[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; - } - - &[role='option']: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; - align-items: center; - > div { - flex: 1; - } -`; diff --git a/src/components/List/ListItem/ListItem.tsx b/src/components/List/ListItem/ListItem.tsx deleted file mode 100644 index 2707895de..000000000 --- a/src/components/List/ListItem/ListItem.tsx +++ /dev/null @@ -1,50 +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 = { - /** Content of the ListItem */ - content: ListItemType; - /** 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; - /** Determines if the item is a header of a section */ - isGroupItem?: boolean; -} & TestProps & - Omit, 'value'>; - -const ListItem = React.forwardRef( - ( - { content, isDisabled = false, isHighlighted = false, searchTerm, dataTestId, ...rest }, - ref - ) => { - return ( -
    • -
      - -
      -
    • - ); - } -); -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/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/Window.tsx b/src/components/List/Window.tsx index f683d2d56..ad5dfcf32 100644 --- a/src/components/List/Window.tsx +++ b/src/components/List/Window.tsx @@ -1,4 +1,3 @@ -import { mergeProps } from '@react-aria/utils'; import useElementSize from 'hooks/useElementSize'; import { throttle } from 'lodash'; import React, { forwardRef } from 'react'; @@ -14,6 +13,11 @@ export type WindowProps = { 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(); @@ -23,18 +27,7 @@ const Window = forwardRef( // get the children to be renderd const visibleChildren = React.useMemo(() => { if (!isVirtualizationEnabled) { - return children.map((child, index) => - React.cloneElement(child, { - // style: { - // position: 'absolute', - // top: index * rowHeight + index * gap, - // height: rowHeight, - // left: 0, - // right: 0, - // lineHeight: `${rowHeight}px`, - // }, - }) - ); + return children.map((child, index) => React.cloneElement(child, {})); } const startIndex = Math.max(Math.floor(scrollPosition / rowHeight) - bufferedItems, 0); const endIndex = Math.min( @@ -63,6 +56,7 @@ const Window = forwardRef( setScrollPosition(e.target.scrollTop); }, 50, + // eslint-disable-next-line @typescript-eslint/naming-convention { leading: false } ), [] 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..3740767e2 --- /dev/null +++ b/src/components/List/components/ListItemAction/ListItemAction.style.ts @@ -0,0 +1,11 @@ +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; + //min-width: ${rem(36)}; + `; 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..d82ae3df7 --- /dev/null +++ b/src/components/List/components/ListItemText/ListItemText.style.ts @@ -0,0 +1,26 @@ +import styled from '@emotion/styled'; +import { rem } from 'theme/utils'; + +export const ListItemTextWrapper = styled.div<{ 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..3769e9bba --- /dev/null +++ b/src/components/List/components/ListItemWrapper/ListItemWrapper.style.ts @@ -0,0 +1,69 @@ +import { css, SerializedStyles } from '@emotion/react'; +import { Theme } from 'theme'; +import { rem } from 'theme/utils'; + +import { ListRowSize } from '../../types'; +import { ListItemTextWrapper } from '../ListItemText/ListItemText.style'; +import { body02, label02, body03, label03 } from 'components/Typography/Typography.config.styles'; + +export const listItemWrapperStyle = + ({ rowSize, isDisabled }: { rowSize?: ListRowSize; isDisabled: boolean }) => + (theme: Theme): SerializedStyles => { + const isCompact = rowSize === 'compact'; + const height = isCompact ? rem(40) : rem(52); + 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)}; + } + + &[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)}; + } + } + + ${!isDisabled && + ` + &[role='option']:hover { + background-color: ${theme.utils.getColor('lightGrey', 50)}; + cursor: pointer; + } + `} + + ${isDisabled && + ` + opacity: 0.5; + cursor: not-allowed; + `} + `; + }; diff --git a/src/components/List/components/ListItemWrapper/ListItemWrapper.tsx b/src/components/List/components/ListItemWrapper/ListItemWrapper.tsx new file mode 100644 index 000000000..23a792f4f --- /dev/null +++ b/src/components/List/components/ListItemWrapper/ListItemWrapper.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { TestProps } from 'utils/types'; + +import { listItemWrapperStyle } 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..11ae610aa --- /dev/null +++ b/src/components/List/stories/List.stories.mdx @@ -0,0 +1,140 @@ +import { Meta, Story, Preview, Props } from '@storybook/addon-docs'; +import { select, number, boolean } from '@storybook/addon-knobs'; +import ListShowcase from '../../storyUtils/ListShowcase'; +import List, { ListSection, ListItemText, ListItem } from '../index'; +import { FIGMA_URL, Function } from '../../../utils/common'; +import SectionHeader from '../../../storybook/SectionHeader'; +import { useState } from 'react'; + + + + + +- [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..6b1b9643e --- /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/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/Menu/Menu.tsx b/src/components/Menu/Menu.tsx index 5dc9ca541..3e14f6766 100644 --- a/src/components/Menu/Menu.tsx +++ b/src/components/Menu/Menu.tsx @@ -1,21 +1,18 @@ import useTheme from 'hooks/useTheme'; -import { useTypeColorToColorMatch } from 'hooks/useTypeColorToColorMatch'; -import { isEmpty } from 'lodash'; +import { flatMap, 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 { SELECT_ALL_OPTION } from '../Select/constants'; 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 +77,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([...keys][0]); + 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 9292ff124..321ea44d5 100644 --- a/src/components/Select/Select.stories.mdx +++ b/src/components/Select/Select.stories.mdx @@ -26,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' }, @@ -81,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 @@ -105,6 +101,10 @@ A universal Select component that is a clickable element that can hold many form ]} /> +## Props + + + ## Variants ### Simple Select 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 44b9b60a7..15ca847ee 100644 --- a/src/components/Select/components/SelectMenu/SelectMenu.tsx +++ b/src/components/Select/components/SelectMenu/SelectMenu.tsx @@ -1,9 +1,11 @@ import useCombinedRefs from 'hooks/useCombinedRefs'; +import { flatMap } 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 List, { ListItem, ListItemText, ListSection } from 'components/List'; import { MAX_NON_VIRTUALIZED_ITEMS_SELECT } from 'components/List/utils'; import { SELECT_ALL_OPTION } from 'components/Select/constants'; import { TextInputBaseProps } from 'components/TextInputBase'; @@ -42,15 +44,55 @@ 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([...keys][0]); + if (keyFound === SELECT_ALL_OPTION.value) { + handleOptionClick(SELECT_ALL_OPTION); + } else { + const optionFound = flatMap(filteredOptions, (o) => o.options || o).find( + (o) => 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/storyUtils/ListShowcase.tsx b/src/components/storyUtils/ListShowcase.tsx deleted file mode 100644 index 917bbe6ad..000000000 --- a/src/components/storyUtils/ListShowcase.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import uniqueId from 'lodash/uniqueId'; -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: uniqueId(), label: 'Option 1 of Group ' + index }, - { value: uniqueId(), label: 'Option 2 of Group ' + index }, - ] - : undefined, - }; - }); - - return ( - - ); -}; - -export default ListShowcase; 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" From 76b4764a82eff5fbad6cba98150887cfc3cea1a4 Mon Sep 17 00:00:00 2001 From: panagiotis vourtsis Date: Thu, 27 Jul 2023 15:29:30 +0300 Subject: [PATCH 03/15] feat(list): update tests and documentation for all --- .../DropdownButton/DropdownButton.tsx | 3 +- src/components/Filter/Filter.test.tsx | 9 +- .../Filter/components/Options/Options.tsx | 6 +- src/components/List/List.tsx | 19 +- .../List/__snapshots__/List.stories.storyshot | 1941 ----------------- .../ListItemAction/ListItemAction.style.ts | 1 - .../ListItemText/ListItemText.style.ts | 6 +- .../ListItemWrapper/ListItemWrapper.style.ts | 103 +- .../ListItemWrapper/ListItemWrapper.tsx | 15 +- src/components/List/stories/List.stories.mdx | 3 - .../List/stories/ListItemText.stories.mdx | 2 +- .../__snapshots__/List.stories.storyshot | 1282 +++++++++++ .../__snapshots__/ListItem.stories.storyshot | 1216 +++++++++++ .../ListItemText.stories.storyshot | 565 +++++ .../ListSection.stories.storyshot | 775 +++++++ src/components/List/utils.tsx | 67 +- src/components/Menu/Menu.tsx | 5 +- src/components/Select/Select.test.tsx | 3 +- .../components/SelectMenu/SelectMenu.tsx | 11 +- src/storybook.test.ts | 6 +- 20 files changed, 3934 insertions(+), 2104 deletions(-) delete mode 100644 src/components/List/__snapshots__/List.stories.storyshot create mode 100644 src/components/List/stories/__snapshots__/List.stories.storyshot create mode 100644 src/components/List/stories/__snapshots__/ListItem.stories.storyshot create mode 100644 src/components/List/stories/__snapshots__/ListItemText.stories.storyshot create mode 100644 src/components/List/stories/__snapshots__/ListSection.stories.storyshot diff --git a/src/components/DropdownButton/DropdownButton.tsx b/src/components/DropdownButton/DropdownButton.tsx index 606e4d160..5c16867dc 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'; @@ -109,7 +110,7 @@ const DropdownButton = React.forwardRef( label={'dropdown-button'} onSelectionChange={(keys) => { setIsOpen(false); - const keyFound = String([...keys][0]); + const keyFound = String(head(Array.from(keys))); const optionFound = items.find((o) => o === keyFound); optionFound && handleOptionClick(optionFound); }} 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 4b65b6ff0..be62ac844 100644 --- a/src/components/Filter/components/Options/Options.tsx +++ b/src/components/Filter/components/Options/Options.tsx @@ -1,4 +1,4 @@ -import { flatMap } from 'lodash'; +import { flatMap, head } from 'lodash'; import React from 'react'; import { emptyStyle } from './Options.style'; @@ -39,12 +39,12 @@ const Options: React.FC = ({ selectedKeys={selectedItem ? [selectedItem.value] : []} disabledKeys={items.filter((o) => o.isDisabled).map((o) => o.value)} onSelectionChange={(keys) => { - const keyFound = String([...keys][0]); + 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) => o.value === keyFound + (o) => String(o.value) === keyFound ); optionFound && onSelect(optionFound); } diff --git a/src/components/List/List.tsx b/src/components/List/List.tsx index 83fd46838..c4c3f45d2 100644 --- a/src/components/List/List.tsx +++ b/src/components/List/List.tsx @@ -7,7 +7,7 @@ import { AriaListBoxProps, useListBoxSection } from 'react-aria'; import { TestProps } from 'utils/types'; import ListItemWrapper from './components/ListItemWrapper/ListItemWrapper'; -import { listItemWrapperStyle } from './components/ListItemWrapper/ListItemWrapper.style'; +import { ListItemWrapperStyled } from './components/ListItemWrapper/ListItemWrapper.style'; import { listStyle, wrapperStyle } from './List.style'; import { ListSelected, ListSelection } from './types'; import Window from './Window'; @@ -56,6 +56,9 @@ const List = React.forwardRef((props, ref) => { // @ts-ignore const { listBoxProps } = useListBox(props, state, combinedRefs); + const firstKey = state.collection.getFirstKey(); + const first = firstKey ? state.collection.getItem(firstKey) : null; + return (
      @@ -65,10 +68,10 @@ const List = React.forwardRef((props, ref) => { css={listStyle({ width, height })} id={listBoxProps.id} isVirtualizationEnabled={isVirtualized} - rowHeight={40} + rowHeight={first?.props.rowSize === 'compact' ? 40 : 56} ref={combinedRefs} > - {[...state.collection].map((item) => { + {Array.from(state.collection).map((item) => { return item.type === 'section' ? ( ) : ( @@ -117,12 +120,10 @@ function ListBoxSection({ section, state }: any) { return ( <> -
    • {section.rendered && ( @@ -148,7 +149,7 @@ function ListBoxSection({ section, state }: any) { /> ))}
    -
  • + ); } diff --git a/src/components/List/__snapshots__/List.stories.storyshot b/src/components/List/__snapshots__/List.stories.storyshot deleted file mode 100644 index 589919ce8..000000000 --- a/src/components/List/__snapshots__/List.stories.storyshot +++ /dev/null @@ -1,1941 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Storyshots Design System/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 { - min-height: 3.5rem; - color: #212332; - font-size: 1rem; - background-color: white; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - padding: 0 1rem 0px 1rem; - font-weight: initial; -} - -.emotion-3[role='option'] { - cursor: pointer; -} - -.emotion-3>span { - -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; - min-height: 3.5rem; - padding: 0 1rem 0px 1rem; - color: #54587f; -} - -.emotion-3[data-focus-visible] { - background-color: #f3f5f8; -} - -.emotion-3[aria-selected='true'] { - background-color: #e7eefe; - color: #1451dc; - font-weight: 500; -} - -.emotion-3[aria-selected='true'][data-focus-visible] { - background-color: #f3f5f8; -} - -.emotion-3[aria-disabled] { - color: var(--text-color-disabled); -} - -.emotion-3 strong { - font-weight: bold; -} - -.emotion-3[role='option']:hover { - background-color: #f3f5f8; -} - -.emotion-4 { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - cursor: inherit; - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - 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; -} - -.emotion-4>div { - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; -} - -.emotion-5 { - cursor: inherit; -} - -.emotion-6 { - color: #212332; - font-size: 1rem; - background-color: white; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - font-weight: bold; -} - -.emotion-6[role='option'] { - cursor: pointer; -} - -.emotion-6>span { - -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; - min-height: 3.5rem; - padding: 0 1rem 0px 1rem; - color: #54587f; -} - -.emotion-6[data-focus-visible] { - background-color: #f3f5f8; -} - -.emotion-6[aria-selected='true'] { - background-color: #e7eefe; - color: #1451dc; - font-weight: 500; -} - -.emotion-6[aria-selected='true'][data-focus-visible] { - background-color: #f3f5f8; -} - -.emotion-6[aria-disabled] { - color: var(--text-color-disabled); -} - -.emotion-6 strong { - font-weight: bold; -} - -.emotion-6[role='option']:hover { - background-color: #f3f5f8; -} - -
    -
    -
    -
    -
    -
      -
    • -
      -
      - All -
      -
      -
    • -
    • - - Group 0 - -
        -
      • -
        -
        - Option 1 of Group 0 -
        -
        -
      • -
      • -
        -
        - Option 2 of Group 0 -
        -
        -
      • -
      -
    • -
    • - - Group 1 - -
        -
      • -
        -
        - Option 1 of Group 1 -
        -
        -
      • -
      • -
        -
        - Option 2 of Group 1 -
        -
        -
      • -
      -
    • -
    • - - Group 2 - -
        -
      • -
        -
        - Option 1 of Group 2 -
        -
        -
      • -
      • -
        -
        - Option 2 of Group 2 -
        -
        -
      • -
      -
    • -
    • - - Group 3 - -
        -
      • -
        -
        - Option 1 of Group 3 -
        -
        -
      • -
      • -
        -
        - Option 2 of Group 3 -
        -
        -
      • -
      -
    • -
    • - - Group 4 - -
        -
      • -
        -
        - Option 1 of Group 4 -
        -
        -
      • -
      • -
        -
        - Option 2 of Group 4 -
        -
        -
      • -
      -
    • -
    -
    -
    -
    -
    -
    -`; - -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: undefinedpx; -} - -.emotion-3 { - min-height: 3.5rem; - color: #212332; - font-size: 1rem; - background-color: white; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - padding: 0 1rem 0px 1rem; - font-weight: initial; -} - -.emotion-3[role='option'] { - cursor: pointer; -} - -.emotion-3>span { - -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; - min-height: 3.5rem; - padding: 0 1rem 0px 1rem; - color: #54587f; -} - -.emotion-3[data-focus-visible] { - background-color: #f3f5f8; -} - -.emotion-3[aria-selected='true'] { - background-color: #e7eefe; - color: #1451dc; - font-weight: 500; -} - -.emotion-3[aria-selected='true'][data-focus-visible] { - background-color: #f3f5f8; -} - -.emotion-3[aria-disabled] { - color: var(--text-color-disabled); -} - -.emotion-3 strong { - font-weight: bold; -} - -.emotion-3[role='option']:hover { - background-color: #f3f5f8; -} - -.emotion-4 { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - cursor: inherit; - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - 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; -} - -.emotion-4>div { - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; -} - -.emotion-5 { - cursor: inherit; -} - -
    -
    -
    -
    -
    -
      -
    • -
      -
      - All -
      -
      -
    • -
    • -
      -
      - Item 0 -
      -
      -
    • -
    • -
      -
      - Item 1 -
      -
      -
    • -
    • -
      -
      - Item 2 -
      -
      -
    • -
    • -
      -
      - Item 3 -
      -
      -
    • -
    • -
      -
      - Item 4 -
      -
      -
    • -
    -
    -
    -
    -
    -
    -`; - -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: undefinedpx; -} - -.emotion-3 { - min-height: 3.5rem; - color: #212332; - font-size: 1rem; - background-color: white; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - padding: 0 1rem 0px 1rem; - font-weight: initial; -} - -.emotion-3[role='option'] { - cursor: pointer; -} - -.emotion-3>span { - -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; - min-height: 3.5rem; - padding: 0 1rem 0px 1rem; - color: #54587f; -} - -.emotion-3[data-focus-visible] { - background-color: #f3f5f8; -} - -.emotion-3[aria-selected='true'] { - background-color: #e7eefe; - color: #1451dc; - font-weight: 500; -} - -.emotion-3[aria-selected='true'][data-focus-visible] { - background-color: #f3f5f8; -} - -.emotion-3[aria-disabled] { - color: var(--text-color-disabled); -} - -.emotion-3 strong { - font-weight: bold; -} - -.emotion-3[role='option']:hover { - background-color: #f3f5f8; -} - -.emotion-4 { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - cursor: inherit; - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - 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; -} - -.emotion-4>div { - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; -} - -.emotion-5 { - cursor: inherit; -} - -.emotion-9 { - 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-10 { - fill: #5f6c85; - width: 1rem; - height: 1rem; -} - -.emotion-10 path { - fill: #5f6c85; -} - -
    -
    -
    -
    -
    -
      -
    • -
      -
      - All -
      -
      -
    • -
    • -
      -
      - Option 1 -
      - - - -
      -
    • -
    • -
      -
      - Option 2 -
      - - - -
      -
    • -
    • -
      -
      - Option 3 -
      - - - -
      -
    • -
    • -
      -
      - Option 4 -
      - - - -
      -
    • -
    • -
      -
      - Option 5 -
      - - - -
      -
    • -
    -
    -
    -
    -
    -
    -`; - -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 { - min-height: 3.5rem; - color: #212332; - font-size: 1rem; - background-color: white; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - padding: 0 1rem 0px 1rem; - font-weight: initial; -} - -.emotion-3[role='option'] { - cursor: pointer; -} - -.emotion-3>span { - -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; - min-height: 3.5rem; - padding: 0 1rem 0px 1rem; - color: #54587f; -} - -.emotion-3[data-focus-visible] { - background-color: #f3f5f8; -} - -.emotion-3[aria-selected='true'] { - background-color: #e7eefe; - color: #1451dc; - font-weight: 500; -} - -.emotion-3[aria-selected='true'][data-focus-visible] { - background-color: #f3f5f8; -} - -.emotion-3[aria-disabled] { - color: var(--text-color-disabled); -} - -.emotion-3 strong { - font-weight: bold; -} - -.emotion-3[role='option']:hover { - background-color: #f3f5f8; -} - -.emotion-4 { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - cursor: inherit; - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; - 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; -} - -.emotion-4>div { - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; -} - -.emotion-5 { - cursor: inherit; -} - -
    -
    -
    -
    -
    -
      -
    • -
      -
      - All -
      -
      -
    • -
    • -
      -
      - Item 0 -
      -
      -
    • -
    • -
      -
      - Item 1 -
      -
      -
    • -
    • -
      -
      - Item 2 -
      -
      -
    • -
    • -
      -
      - Item 3 -
      -
      -
    • -
    -
    -
    -
    -
    -
    -`; diff --git a/src/components/List/components/ListItemAction/ListItemAction.style.ts b/src/components/List/components/ListItemAction/ListItemAction.style.ts index 3740767e2..a51570760 100644 --- a/src/components/List/components/ListItemAction/ListItemAction.style.ts +++ b/src/components/List/components/ListItemAction/ListItemAction.style.ts @@ -7,5 +7,4 @@ export const listItemActionWrapper = () => (): SerializedStyles => flex-direction: row; align-items: center; justify-content: center; - //min-width: ${rem(36)}; `; diff --git a/src/components/List/components/ListItemText/ListItemText.style.ts b/src/components/List/components/ListItemText/ListItemText.style.ts index d82ae3df7..d6fe387d9 100644 --- a/src/components/List/components/ListItemText/ListItemText.style.ts +++ b/src/components/List/components/ListItemText/ListItemText.style.ts @@ -1,7 +1,9 @@ import styled from '@emotion/styled'; -import { rem } from 'theme/utils'; -export const ListItemTextWrapper = styled.div<{ isGroupItem?: boolean; isHighlighted: boolean }>` +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 }) => diff --git a/src/components/List/components/ListItemWrapper/ListItemWrapper.style.ts b/src/components/List/components/ListItemWrapper/ListItemWrapper.style.ts index 3769e9bba..af3f7fab3 100644 --- a/src/components/List/components/ListItemWrapper/ListItemWrapper.style.ts +++ b/src/components/List/components/ListItemWrapper/ListItemWrapper.style.ts @@ -1,69 +1,64 @@ -import { css, SerializedStyles } from '@emotion/react'; -import { Theme } from 'theme'; +import { css } from '@emotion/react'; +import styled from '@emotion/styled'; import { rem } from 'theme/utils'; import { ListRowSize } from '../../types'; import { ListItemTextWrapper } from '../ListItemText/ListItemText.style'; import { body02, label02, body03, label03 } from 'components/Typography/Typography.config.styles'; -export const listItemWrapperStyle = - ({ rowSize, isDisabled }: { rowSize?: ListRowSize; isDisabled: boolean }) => - (theme: Theme): SerializedStyles => { - const isCompact = rowSize === 'compact'; - const height = isCompact ? rem(40) : rem(52); - const padding = css`0 ${theme.globals.spacing.get('5')}`; - const itemTypographyStyle = isCompact ? body03(theme) : body02(theme); +export const ListItemWrapperStyled = styled('li', { target: '' })<{ + rowSize?: ListRowSize; + isDisabled: boolean; +}>(({ rowSize, isDisabled, theme }) => { + const isCompact = rowSize === 'compact'; + const height = isCompact ? rem(40) : rem(52); + 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}; - } + 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)}; - } + 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)}; - &[data-focus-visible] { - background-color: ${theme.utils.getColor('lightGrey', 50)}; + &:hover { + background-color: ${!isDisabled ? theme.utils.getColor('lightGrey', 50) : undefined}; + cursor: ${!isDisabled ? 'pointer' : 'initial'}; } + } - &[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)}; + } - &[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)} } - ${!isDisabled && - ` - &[role='option']:hover { - background-color: ${theme.utils.getColor('lightGrey', 50)}; - cursor: pointer; - } - `} + &[data-focus-visible] { + background-color: ${theme.utils.getColor('lightGrey', 50)}; + } + } - ${isDisabled && - ` - opacity: 0.5; - cursor: not-allowed; - `} - `; - }; + 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 index 23a792f4f..e90415079 100644 --- a/src/components/List/components/ListItemWrapper/ListItemWrapper.tsx +++ b/src/components/List/components/ListItemWrapper/ListItemWrapper.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { TestProps } from 'utils/types'; -import { listItemWrapperStyle } from './ListItemWrapper.style'; +import { ListItemWrapperStyled } from './ListItemWrapper.style'; import { ListRowSize } from '../../types'; export type ListItemProps = { @@ -32,18 +32,15 @@ const ListItemWrapper = React.forwardRef( ref ) => { return ( -
  • {children} -
  • + ); } ); diff --git a/src/components/List/stories/List.stories.mdx b/src/components/List/stories/List.stories.mdx index 11ae610aa..d63ffe76e 100644 --- a/src/components/List/stories/List.stories.mdx +++ b/src/components/List/stories/List.stories.mdx @@ -1,10 +1,7 @@ import { Meta, Story, Preview, Props } from '@storybook/addon-docs'; -import { select, number, boolean } from '@storybook/addon-knobs'; -import ListShowcase from '../../storyUtils/ListShowcase'; import List, { ListSection, ListItemText, ListItem } from '../index'; import { FIGMA_URL, Function } from '../../../utils/common'; import SectionHeader from '../../../storybook/SectionHeader'; -import { useState } from 'react'; `']} + guidelines={['Always use to show text without other custom components like Icon, Button etc']} /> ```js 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..448e421a2 --- /dev/null +++ b/src/components/List/stories/__snapshots__/List.stories.storyshot @@ -0,0 +1,1282 @@ +// 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: #fff; +} + +.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-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; +} + +
    +
    +
    +
    +
    +
      +
    • + + 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: #fff; +} + +.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: #fff; +} + +.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..9db887eb7 --- /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: #fff; +} + +.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..744c18a0a --- /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: #fff; +} + +.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: #fff; +} + +.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..c410f8dd9 --- /dev/null +++ b/src/components/List/stories/__snapshots__/ListSection.stories.storyshot @@ -0,0 +1,775 @@ +// 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: #fff; +} + +.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-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; +} + +
    +
    +
    +
    +
    +
    +
    +
      +
    • + + 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: #fff; +} + +.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-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; +} + +
    +
    +
    +
    +
    +
    +
    +
      +
    • + + Section 1 + +
        +
      • +
        + + ... + +
        +
      • +
      • +
        + + ... + +
        +
      • +
      +
    • +
    • + + Section 2 + +
        +
      • +
        + + ... + +
        +
      • +
      • +
        + + ... + +
        +
      • +
      +
    • +
    +
    +
    +
    +
    +
    +
    +
    +`; diff --git a/src/components/List/utils.tsx b/src/components/List/utils.tsx index f2935aaca..e6eefd86d 100644 --- a/src/components/List/utils.tsx +++ b/src/components/List/utils.tsx @@ -1,11 +1,5 @@ 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'; +// 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; @@ -14,62 +8,3 @@ export const MAX_NON_VIRTUALIZED_ITEMS_SELECT = 5; /** Min-max heights for VList */ 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, - searchTerm, -}: { - content: ListItemType; - searchTerm?: string; -}) => { - if (searchTerm && 'label' in content && content?.label) { - return ( - - ); - } - - return ( - <> -
    {renderLabelWithHelperText(content)}
    - {content?.iconProps && } - - ); -}; diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx index 3e14f6766..f975aa1a9 100644 --- a/src/components/Menu/Menu.tsx +++ b/src/components/Menu/Menu.tsx @@ -1,5 +1,5 @@ import useTheme from 'hooks/useTheme'; -import { flatMap, isEmpty } from 'lodash'; +import { head, isEmpty } from 'lodash'; import * as React from 'react'; import { EventProps } from 'utils/common'; @@ -7,7 +7,6 @@ import { wrapperStyle } from './Menu.style'; import { TestProps } from '../../utils/types'; import Button from '../Button'; import { AcceptedIconNames } from '../Icon/types'; -import { SELECT_ALL_OPTION } from '../Select/constants'; import ClickAwayListener from '../utils/ClickAwayListener'; import { optionsStyle, MenuPositionAllowed } from '../utils/DropdownOptions'; import { AvatarColors } from 'components/Avatar'; @@ -80,7 +79,7 @@ const Menu: React.FC = (props) => { label={'filter-options'} onSelectionChange={(keys) => { setIsOpen(false); - const keyFound = String([...keys][0]); + const keyFound = String(head(Array.from(keys))); const optionFound = items.find((o) => o === keyFound); optionFound && onSelect(optionFound); }} diff --git a/src/components/Select/Select.test.tsx b/src/components/Select/Select.test.tsx index 51b6a7798..f5905f397 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(), @@ -365,7 +366,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}`)); screen.debug(); diff --git a/src/components/Select/components/SelectMenu/SelectMenu.tsx b/src/components/Select/components/SelectMenu/SelectMenu.tsx index 15ca847ee..918259159 100644 --- a/src/components/Select/components/SelectMenu/SelectMenu.tsx +++ b/src/components/Select/components/SelectMenu/SelectMenu.tsx @@ -1,5 +1,5 @@ import useCombinedRefs from 'hooks/useCombinedRefs'; -import { flatMap } from 'lodash'; +import { flatMap, head } from 'lodash'; import uniqueId from 'lodash/uniqueId'; import React, { forwardRef, useEffect, useRef } from 'react'; @@ -47,15 +47,14 @@ const SelectMenu = forwardRef((props, ref) => label={uniqueId('menu_list')} ref={combinedRefs} height={5 * 40} - // rowSize={'small'} isVirtualized={isVirtualized && filteredOptions.length > MAX_NON_VIRTUALIZED_ITEMS_SELECT} onSelectionChange={(keys) => { - const keyFound = String([...keys][0]); + 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) => o.value === keyFound + (o) => String(o.value) === keyFound ); optionFound && handleOptionClick(optionFound); } @@ -79,7 +78,7 @@ const SelectMenu = forwardRef((props, ref) => {option.options.map((o) => ( - {o.label} + {o.label} ))} @@ -88,7 +87,7 @@ const SelectMenu = forwardRef((props, ref) => return ( - {option.label} + {option.label} ); })} 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: () => {}, From ae980e714e4278c021bd889862f5845aa3199fff Mon Sep 17 00:00:00 2001 From: panagiotis vourtsis Date: Thu, 27 Jul 2023 15:45:58 +0300 Subject: [PATCH 04/15] chore: remove isDefaultOption as it is not needed --- src/components/Select/constants.ts | 1 - src/components/Select/types.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/components/Select/constants.ts b/src/components/Select/constants.ts index e2b06b81f..6e8b064e9 100644 --- a/src/components/Select/constants.ts +++ b/src/components/Select/constants.ts @@ -3,5 +3,4 @@ import { SelectOption } from './types'; export const SELECT_ALL_OPTION: SelectOption = { value: 'select_all', label: 'Select All', - isDefaultOption: true, } as const; diff --git a/src/components/Select/types.ts b/src/components/Select/types.ts index 7d1c9c488..1d44d8991 100644 --- a/src/components/Select/types.ts +++ b/src/components/Select/types.ts @@ -17,7 +17,6 @@ export type SelectOptionBase = { tooltipInfo?: string; options?: SelectOption[]; isCreated?: boolean; - isDefaultOption?: boolean; }; export type SelectOption = SelectOptionBase & SelectOptionValues; From 3b16bc4ae94f7c2e343aaf69ff08fb13226e39d5 Mon Sep 17 00:00:00 2001 From: panagiotis vourtsis Date: Mon, 31 Jul 2023 12:15:23 +0300 Subject: [PATCH 05/15] chore(List): update onSelecitonChange callback with named functions --- .../DropdownButton/DropdownButton.tsx | 19 +++++++----- .../Filter/components/Options/Options.tsx | 30 +++++++++++-------- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/components/DropdownButton/DropdownButton.tsx b/src/components/DropdownButton/DropdownButton.tsx index 5c16867dc..893ff7ef2 100644 --- a/src/components/DropdownButton/DropdownButton.tsx +++ b/src/components/DropdownButton/DropdownButton.tsx @@ -14,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, { ListItem, ListItemText, 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'; @@ -68,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)}>
    @@ -108,12 +118,7 @@ const DropdownButton = React.forwardRef( {items && ( { - setIsOpen(false); - const keyFound = String(head(Array.from(keys))); - const optionFound = items.find((o) => o === keyFound); - optionFound && handleOptionClick(optionFound); - }} + onSelectionChange={onSelectionChange} dataTestId={generateTestDataId('dropdown-button-options', dataTestPrefixId)} > {items.map((item) => ( diff --git a/src/components/Filter/components/Options/Options.tsx b/src/components/Filter/components/Options/Options.tsx index be62ac844..4338ae55b 100644 --- a/src/components/Filter/components/Options/Options.tsx +++ b/src/components/Filter/components/Options/Options.tsx @@ -1,11 +1,11 @@ import { flatMap, head } from 'lodash'; -import React from 'react'; +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, { ListItem, ListItemText } from 'components/List'; +import List, { ListItem, ListItemText, ListSelection } from 'components/List'; import { MAX_NON_VIRTUALIZED_ITEMS_FILTER } from 'components/List/utils'; export interface Props { @@ -32,23 +32,27 @@ 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 ? ( o.isDisabled).map((o) => o.value)} - onSelectionChange={(keys) => { - 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); - } - }} + onSelectionChange={onSelectionChange} isVirtualized={isVirtualized && isForcedVirtualized} height={height} dataTestId={dataTestId} From 448650c8b6b57c71be2e74c89a0e93a88c23f2a8 Mon Sep 17 00:00:00 2001 From: panagiotis vourtsis Date: Mon, 31 Jul 2023 12:17:28 +0300 Subject: [PATCH 06/15] chore(List): fix background with theme.globals.colors.white for consistency --- src/components/List/List.style.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/List/List.style.ts b/src/components/List/List.style.ts index 9f09bd379..1268d8dae 100644 --- a/src/components/List/List.style.ts +++ b/src/components/List/List.style.ts @@ -33,7 +33,7 @@ export const listStyle = height: ${height ? rem(height) : 'auto'}; overflow: auto; overflow-x: hidden; - background: #fff; + background: ${theme.globals.colors.white}; `; export const listLabelHelperText = (theme: Theme): SerializedStyles => css` From e66fc4cf6560ef0e71db655c084ebbf3d27ed680 Mon Sep 17 00:00:00 2001 From: panagiotis vourtsis Date: Mon, 31 Jul 2023 12:19:41 +0300 Subject: [PATCH 07/15] chore(List): add label prop description --- src/components/List/List.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/List/List.tsx b/src/components/List/List.tsx index c4c3f45d2..68ff0bc5a 100644 --- a/src/components/List/List.tsx +++ b/src/components/List/List.tsx @@ -15,6 +15,7 @@ import useCombinedRefs from '../../hooks/useCombinedRefs'; import { SelectOption } from '../Select'; export type ListProps = { + /** The label that describes the List, useful to determine aria and accessibility of the list */ label: string; /** Width of the list */ width?: number; From 514f83b090085ac1faae7a8296bf95ef11827a4e Mon Sep 17 00:00:00 2001 From: panagiotis vourtsis Date: Mon, 31 Jul 2023 12:22:25 +0300 Subject: [PATCH 08/15] chore(List): leave a TODO for list on searchTerm and isSearchable props --- src/components/List/List.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/List/List.tsx b/src/components/List/List.tsx index 68ff0bc5a..098b1eaee 100644 --- a/src/components/List/List.tsx +++ b/src/components/List/List.tsx @@ -29,6 +29,7 @@ export type ListProps = { 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 **/ From 37ecdeae6f568ec5c3495ef76e0914fa9062e595 Mon Sep 17 00:00:00 2001 From: panagiotis vourtsis Date: Mon, 31 Jul 2023 12:29:47 +0300 Subject: [PATCH 09/15] docs(List): adding documentation explaining the reasoning on one component file for many --- src/components/List/List.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/components/List/List.tsx b/src/components/List/List.tsx index 098b1eaee..da34a72f5 100644 --- a/src/components/List/List.tsx +++ b/src/components/List/List.tsx @@ -38,6 +38,11 @@ export type ListProps = { TestProps & Omit, 'onChange'>; +/** + * 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, @@ -88,6 +93,9 @@ const List = React.forwardRef((props, ref) => { }); List.displayName = 'List'; +/** + * 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); @@ -114,6 +122,10 @@ function Option({ item, state, style }: { item: any; state: any; style?: any }) ); } +/** + * 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, From c11cdfc3afa1dda0bb001b051c48883e1a5b3675 Mon Sep 17 00:00:00 2001 From: panagiotis vourtsis Date: Mon, 31 Jul 2023 12:39:07 +0300 Subject: [PATCH 10/15] style(List): change style to css prop --- src/components/List/List.style.ts | 20 ++----- src/components/List/List.tsx | 11 +--- .../__snapshots__/List.stories.storyshot | 45 +++++--------- .../__snapshots__/ListItem.stories.storyshot | 2 +- .../ListItemText.stories.storyshot | 4 +- .../ListSection.stories.storyshot | 60 +++++++------------ 6 files changed, 45 insertions(+), 97 deletions(-) diff --git a/src/components/List/List.style.ts b/src/components/List/List.style.ts index 1268d8dae..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 => @@ -36,9 +26,9 @@ export const listStyle = 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 da34a72f5..9092688b0 100644 --- a/src/components/List/List.tsx +++ b/src/components/List/List.tsx @@ -8,7 +8,7 @@ import { TestProps } from 'utils/types'; import ListItemWrapper from './components/ListItemWrapper/ListItemWrapper'; import { ListItemWrapperStyled } from './components/ListItemWrapper/ListItemWrapper.style'; -import { listStyle, wrapperStyle } from './List.style'; +import { groupedUlStyle, listStyle, wrapperStyle } from './List.style'; import { ListSelected, ListSelection } from './types'; import Window from './Window'; import useCombinedRefs from '../../hooks/useCombinedRefs'; @@ -144,14 +144,7 @@ function ListBoxSection({ section, state }: any) { {section.rendered} )} -
      +
        {[...section.childNodes].map((node) => (