diff --git a/.github/workflows/pr-visual-tests.yml b/.github/workflows/pr-visual-tests.yml index 9693610dbb..8c46ec5a54 100644 --- a/.github/workflows/pr-visual-tests.yml +++ b/.github/workflows/pr-visual-tests.yml @@ -7,6 +7,8 @@ jobs: visual_tests: name: Visual Tests runs-on: ubuntu-latest + container: + image: mcr.microsoft.com/playwright:v1.42.1-jammy steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -15,8 +17,6 @@ jobs: cache: npm - name: Install Packages run: npm ci - - name: Install Playwright Browsers - run: npx playwright install --with-deps - name: Run Visual Tests run: npm run playwright env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b411f9a05..373bda32e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## [6.23.0](https://github.com/gravity-ui/uikit/compare/v6.22.0...v6.23.0) (2024-08-06) + + +### Features + +* **ListItemView:** ability to pass custom react node as a content prop ([#1726](https://github.com/gravity-ui/uikit/issues/1726)) ([5d5417a](https://github.com/gravity-ui/uikit/commit/5d5417a35357c85a552ee766f6761c5db5e48974)) +* **useList:** added migration guide to set of useList components ([#1728](https://github.com/gravity-ui/uikit/issues/1728)) ([69283f1](https://github.com/gravity-ui/uikit/commit/69283f1f681ab79023544ce0fb21e7253e3281d4)) + + +### Bug Fixes + +* **Icon:** correctly parse function with default props ([#1713](https://github.com/gravity-ui/uikit/issues/1713)) ([b7eef14](https://github.com/gravity-ui/uikit/commit/b7eef14e0d08ccb81561f111ec544ba9ef6d36f8)) +* **TreeSelect:** fix crashes while has selected elements and has no items ([#1727](https://github.com/gravity-ui/uikit/issues/1727)) ([0e22bde](https://github.com/gravity-ui/uikit/commit/0e22bdeed87b8841d8190758df0a6e4cf0bebd15)) +* **typography:** use correct variables across the project ([#1712](https://github.com/gravity-ui/uikit/issues/1712)) ([4a9a6d1](https://github.com/gravity-ui/uikit/commit/4a9a6d1896e0e4fc773ac66815fc5fa1418176c1)) + +## [6.22.0](https://github.com/gravity-ui/uikit/compare/v6.21.0...v6.22.0) (2024-07-27) + + +### Features + +* **AvatarStack:** add component ([#924](https://github.com/gravity-ui/uikit/issues/924)) ([862f4fb](https://github.com/gravity-ui/uikit/commit/862f4fbbaca8d431f0562965b3cad56e21b7130c)) + + +### Bug Fixes + +* **Breadcrums:** support react <18 types ([#1722](https://github.com/gravity-ui/uikit/issues/1722)) ([5229e99](https://github.com/gravity-ui/uikit/commit/5229e99731ceef030bd19871d39352081fb4ed69)) +* **useList:** fix useList hook for support initialization state ([#1719](https://github.com/gravity-ui/uikit/issues/1719)) ([2b04b18](https://github.com/gravity-ui/uikit/commit/2b04b18e1f1634a358e245b79aa8da65d33a9e95)) + ## [6.21.0](https://github.com/gravity-ui/uikit/compare/v6.20.1...v6.21.0) (2024-07-16) diff --git a/CODEOWNERS b/CODEOWNERS index fa0631e2e0..ccc8c5144a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -24,7 +24,9 @@ /src/components/Loader @SeqviriouM /src/components/Menu @NikitaCG /src/components/Modal @amje +/src/components/Overlay @Vladeeg /src/components/Pagination @jhoncool +/src/components/Palette @Ruminat /src/components/PinInput @amje /src/components/Popover @kseniya57 /src/components/Popup @amje diff --git a/package-lock.json b/package-lock.json index a28de9d6a1..afbfc68d11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@gravity-ui/uikit", - "version": "6.21.0", + "version": "6.23.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gravity-ui/uikit", - "version": "6.21.0", + "version": "6.23.0", "license": "MIT", "dependencies": { "@bem-react/classname": "^1.6.0", diff --git a/package.json b/package.json index 096cfcc0a7..b91695247c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gravity-ui/uikit", - "version": "6.21.0", + "version": "6.23.0", "description": "Gravity UI base styling and components", "keywords": [ "component", diff --git a/src/components/Card/README.md b/src/components/Card/README.md index 1cf9945617..8d173c8f4c 100644 --- a/src/components/Card/README.md +++ b/src/components/Card/README.md @@ -44,12 +44,12 @@ const style = { justifyContent: 'center'; } -Normal -Info -Success -Warning -Danger -Utility +Normal +Info +Success +Warning +Danger +Utility `}>
@@ -83,9 +83,9 @@ const style = { justifyContent: 'center'; } - Container - action with onClick - Selection + Container + action with onClick + Selection `}>
Container @@ -116,10 +116,10 @@ const style = { justifyContent: 'center'; } - Clear - Outlined - Filled - Raised + Clear + Outlined + Filled + Raised `}>
Clear diff --git a/src/components/Hotkey/Hotkey.scss b/src/components/Hotkey/Hotkey.scss index b8ef27401d..8a44c767a3 100644 --- a/src/components/Hotkey/Hotkey.scss +++ b/src/components/Hotkey/Hotkey.scss @@ -10,7 +10,6 @@ $block: '.#{variables.$ns}hotkey'; &, kbd { @include mixins.text-body-1(); - font-family: var(--g-font-family-sans); } &_view { diff --git a/src/components/Icon/utils.ts b/src/components/Icon/utils.ts index ce14fa4727..9eaeae04c6 100644 --- a/src/components/Icon/utils.ts +++ b/src/components/Icon/utils.ts @@ -15,7 +15,7 @@ export function isSvgrData(data: SVGIconData): data is SVGIconSvgrData { } export function isComponentSvgData(data: SVGIconData): data is SVGIconComponentData { - return typeof data === 'object' && 'defaultProps' in data; + return (typeof data === 'object' || typeof data === 'function') && 'defaultProps' in data; } export function isStringSvgData(data: SVGIconData): data is SVGIconStringData { diff --git a/src/components/Overlay/__stories__/Overlay.stories.tsx b/src/components/Overlay/__stories__/Overlay.stories.tsx index edfefbe88d..724481be35 100644 --- a/src/components/Overlay/__stories__/Overlay.stories.tsx +++ b/src/components/Overlay/__stories__/Overlay.stories.tsx @@ -24,7 +24,7 @@ const b = block('overlay-stories'); type Story = StoryObj; export default { - title: 'Components/Utils/Overlay', + title: 'Components/Feedback/Overlay', component: Overlay, } as Meta; diff --git a/src/components/Table/__stories__/Table.stories.tsx b/src/components/Table/__stories__/Table.stories.tsx index 144bc1b0b4..ca6e588c39 100644 --- a/src/components/Table/__stories__/Table.stories.tsx +++ b/src/components/Table/__stories__/Table.stories.tsx @@ -164,7 +164,7 @@ const WithTableActionsTemplate: StoryFn> = (args) => { ({title})} + mapItemDataToContentProps={(title) => ({title})} title="Actions select example" /> ); diff --git a/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx b/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx index 6cca789c33..840921b0a7 100644 --- a/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx +++ b/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx @@ -25,7 +25,7 @@ import type { } from '../../../../TreeSelect/types'; import {TextInput} from '../../../../controls/TextInput'; import {Flex} from '../../../../layout/Flex/Flex'; -import type {ListItemCommonProps, ListItemViewProps} from '../../../../useList'; +import type {ListItemViewContentType, ListItemViewProps} from '../../../../useList'; import {ListContainerView, ListItemView, useListFilter} from '../../../../useList'; import {block} from '../../../../utils/cn'; import type {TableColumnConfig} from '../../../Table'; @@ -190,16 +190,18 @@ const useDndRenderItem = (sortable: boolean | undefined) => { }) => { const isDragDisabled = sortable === false || renderContainerProps?.isDragDisabled === true; const endSlot = isDragDisabled ? undefined : ; - const hasSelectionIcon = !item.isRequired; const startSlot = item.isRequired ? : undefined; const selected = item.isRequired ? false : props.selected; const commonProps: ListItemViewProps = { ...props, selected, - startSlot, - hasSelectionIcon, - endSlot, + selectionViewType: item.isRequired ? 'single' : 'multiple', + content: { + ...props.content, + startSlot, + endSlot, + }, }; if (isDragDisabled) { @@ -241,7 +243,7 @@ export type TableColumnSetupItem = TableSetting & { sticky?: TableColumnConfig['sticky']; }; -const mapItemDataToProps = (item: TableColumnSetupItem): ListItemCommonProps => { +const mapItemDataToContentProps = (item: TableColumnSetupItem): ListItemViewContentType => { return { title: item.title, }; @@ -435,7 +437,7 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => { return ( ({ renderItem: propsRenderItem, renderContainer = ListContainer, onItemClick: propsOnItemClick, - mapItemDataToProps, + mapItemDataToContentProps, }: TreeListProps) => { const uniqId = useUniqId(); const treeListId = id ?? uniqId; @@ -71,7 +71,7 @@ export const TreeList = ({ id: itemId, size, multiple, - mapItemDataToProps, + mapItemDataToContentProps, onItemClick, list, }); diff --git a/src/components/TreeList/__stories__/TreeListDocs.md b/src/components/TreeList/__stories__/TreeListDocs.md index 1655bfbe67..c1009f7018 100644 --- a/src/components/TreeList/__stories__/TreeListDocs.md +++ b/src/components/TreeList/__stories__/TreeListDocs.md @@ -25,7 +25,7 @@ const items: ListItemType[] = ['one', 'two', 'free', 'four', 'five']; const list = useList({items}); - ({title: item})} />; + ({title: item})} />; ``` ### Example with state: @@ -56,7 +56,7 @@ const Component = () => { ({title})} + mapItemDataToContentProps={({title}) => ({title})} /> ); }; @@ -64,18 +64,18 @@ const Component = () => { ## Props: -| Name | Description | Type | Default | -| :----------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------: | :-----: | -| list | result of [list](/docs/lab-uselist--docs#uselist) hook. | `UseListResult` | | -| containerRef | a reference to the DOM element of the List container inside which to search for its elements; | `React.RefObject` | | -| qa | Selector for tests | `string` | | -| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | -| mapItemDataToProps | Map list item data (`T`) to `ListItemView` props | `(data: T) => ListItemCommonProps` | | -| multiple | One or multiple elements selected list | `boolean` | `false` | -| id | id attribute | `string` | | -| renderItem | Redefine the rendering of a list item. For example, add dividers between list items or wrap an item in a link component. As a view component to display a list item, use [ListItemView](/docs/lab-uselist--docs#listitemview); | `(props: TreeListRenderItem) => React.JSX.Element` | | -| renderContainer | Render custom list container. | `(props: TreeListRenderContainer) => React.JSX.Element` | | -| onItemClick | Override default on click behavior. Pass `null` to disable on click handler | `(props: {id: ListItemId; list: UseListResult}, e: React.SyntheticEvent) => void \| null` | | +| Name | Description | Type | Default | +| :------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------: | :-----: | +| list | result of [list](/docs/lab-uselist--docs#uselist) hook. | `UseListResult` | | +| containerRef | a reference to the DOM element of the List container inside which to search for its elements; | `React.RefObject` | | +| qa | Selector for tests | `string` | | +| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | +| mapItemDataToContentProps | Map list item data (`T`) to `ListItemView` `content` prop | `(data: T) => ListItemViewContentProps` | | +| multiple | One or multiple elements selected list | `boolean` | `false` | +| id | id attribute | `string` | | +| renderItem | Redefine the rendering of a list item. For example, add dividers between list items or wrap an item in a link component. As a view component to display a list item, use [ListItemView](/docs/lab-uselist--docs#listitemview); | `(props: TreeListRenderItem) => React.JSX.Element` | | +| renderContainer | Render custom list container. | `(props: TreeListRenderContainer) => React.JSX.Element` | | +| onItemClick | Override default on click behavior. Pass `null` to disable on click handler | `(props: {id: ListItemId; list: UseListResult}, e: React.SyntheticEvent) => void \| null` | | ### TreeListRenderItem props: diff --git a/src/components/TreeList/__stories__/stories/DefaultStory.tsx b/src/components/TreeList/__stories__/stories/DefaultStory.tsx index e3a63f60e9..df90be1e4e 100644 --- a/src/components/TreeList/__stories__/stories/DefaultStory.tsx +++ b/src/components/TreeList/__stories__/stories/DefaultStory.tsx @@ -12,7 +12,7 @@ function identity(value: T): T { } export interface DefaultStoryProps - extends Omit, 'items' | 'mapItemDataToProps'> { + extends Omit, 'items' | 'mapItemDataToContentProps'> { itemsCount?: number; } @@ -34,7 +34,7 @@ export const DefaultStory = ({itemsCount = 5, ...props}: DefaultStoryProps) => { {...props} list={listWithGroups} onItemClick={null} - mapItemDataToProps={identity} + mapItemDataToContentProps={identity} /> @@ -46,7 +46,7 @@ export const DefaultStory = ({itemsCount = 5, ...props}: DefaultStoryProps) => { {...props} list={listWithNoGroups} onItemClick={null} - mapItemDataToProps={identity} + mapItemDataToContentProps={identity} /> diff --git a/src/components/TreeList/__stories__/stories/InfinityScrollStory.tsx b/src/components/TreeList/__stories__/stories/InfinityScrollStory.tsx index b0dda4d8a7..b3a11c6bc0 100644 --- a/src/components/TreeList/__stories__/stories/InfinityScrollStory.tsx +++ b/src/components/TreeList/__stories__/stories/InfinityScrollStory.tsx @@ -21,7 +21,7 @@ function identity(value: T): T { export interface InfinityScrollStoryProps extends Omit< TreeListProps, - 'value' | 'onUpdate' | 'items' | 'multiple' | 'size' | 'mapItemDataToProps' + 'value' | 'onUpdate' | 'items' | 'multiple' | 'size' | 'mapItemDataToContentProps' > { itemsCount?: number; } @@ -44,13 +44,18 @@ export const InfinityScrollStory = ({itemsCount = 3, ...storyProps}: InfinityScr {...storyProps} size="l" list={list} - mapItemDataToProps={identity} + mapItemDataToContentProps={identity} multiple={multiple} renderItem={({props, context: {isLastItem, childrenIds}}) => { const node = ( {childrenIds.length} : undefined} + content={{ + ...props.content, + endSlot: childrenIds ? ( + + ) : undefined, + }} /> ); diff --git a/src/components/TreeList/__stories__/stories/WithDisabledElementsStory.tsx b/src/components/TreeList/__stories__/stories/WithDisabledElementsStory.tsx index d79539aaa9..50c8f06b97 100644 --- a/src/components/TreeList/__stories__/stories/WithDisabledElementsStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithDisabledElementsStory.tsx @@ -8,7 +8,7 @@ import {TreeList} from '../../TreeList'; import type {TreeListProps} from '../../types'; export interface WithDisabledElementsStoryProps - extends Omit, 'items' | 'mapItemDataToProps'> {} + extends Omit, 'items' | 'mapItemDataToContentProps'> {} const items: ListItemType<{text: string}>[] = [ { @@ -50,7 +50,7 @@ export const WithDisabledElementsStory = ({...storyProps}: WithDisabledElementsS {...storyProps} list={list} containerRef={containerRef} - mapItemDataToProps={({text}) => ({title: text})} + mapItemDataToContentProps={({text}) => ({title: text})} onItemClick={({id}) => { getListItemClickHandler({list})({id}); alert( diff --git a/src/components/TreeList/__stories__/stories/WithDndListStory.tsx b/src/components/TreeList/__stories__/stories/WithDndListStory.tsx index 22bcb3827d..2aaf2d0ec7 100644 --- a/src/components/TreeList/__stories__/stories/WithDndListStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithDndListStory.tsx @@ -42,7 +42,7 @@ const randomItems: CustomDataType[] = createRandomizedData({ }).map(({data}, idx) => ({someRandomKey: data, id: String(idx)})); export interface WithDndListStoryProps - extends Omit, 'items' | 'mapItemDataToProps'> {} + extends Omit, 'items' | 'mapItemDataToContentProps'> {} export const WithDndListStory = (storyProps: WithDndListStoryProps) => { const [items, setItems] = React.useState(randomItems); @@ -117,10 +117,13 @@ export const WithDndListStory = (storyProps: WithDndListStoryProps) => { index, renderContainerProps, }) => { - const commonProps = { + const commonProps: ListItemViewProps = { ...props, - title: data.someRandomKey, - endSlot: , + content: { + ...props.content, + title: data.someRandomKey, + endSlot: , + }, }; // here passed props from `renderContainer` method. @@ -151,7 +154,7 @@ export const WithDndListStory = (storyProps: WithDndListStoryProps) => { {...storyProps} list={list} containerRef={containerRef} - mapItemDataToProps={({someRandomKey}) => ({title: someRandomKey})} + mapItemDataToContentProps={({someRandomKey}) => ({title: someRandomKey})} renderContainer={renderContainer} renderItem={renderItem} /> diff --git a/src/components/TreeList/__stories__/stories/WithFiltrationAndControlsStory.tsx b/src/components/TreeList/__stories__/stories/WithFiltrationAndControlsStory.tsx index a2463b8684..6c77a72dc6 100644 --- a/src/components/TreeList/__stories__/stories/WithFiltrationAndControlsStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithFiltrationAndControlsStory.tsx @@ -15,7 +15,10 @@ interface Entity { } export interface WithFiltrationAndControlsStoryProps - extends Omit, 'value' | 'onUpdate' | 'items' | 'mapItemDataToProps'> { + extends Omit< + TreeListProps, + 'value' | 'onUpdate' | 'items' | 'mapItemDataToContentProps' + > { itemsCount?: number; } @@ -59,7 +62,7 @@ export const WithFiltrationAndControlsStory = ({ x} + mapItemDataToContentProps={(x) => x} renderContainer={renderContainer} /> diff --git a/src/components/TreeList/__stories__/stories/WithGroupSelectionAndCustomIconStory.tsx b/src/components/TreeList/__stories__/stories/WithGroupSelectionAndCustomIconStory.tsx index 0483db50b0..3b1e011016 100644 --- a/src/components/TreeList/__stories__/stories/WithGroupSelectionAndCustomIconStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithGroupSelectionAndCustomIconStory.tsx @@ -6,7 +6,7 @@ import {Button} from '../../../Button'; import {Icon} from '../../../Icon'; import {Flex, spacing} from '../../../layout'; import {ListItemView, useList} from '../../../useList'; -import type {ListItemCommonProps, ListItemId} from '../../../useList'; +import type {ListItemId, ListItemViewContentType} from '../../../useList'; import {createRandomizedData} from '../../../useList/__stories__/utils/makeData'; import {TreeList} from '../../TreeList'; import type {TreeListProps} from '../../types'; @@ -24,12 +24,14 @@ interface CustomDataStructure { export interface WithGroupSelectionAndCustomIconStoryProps extends Omit< TreeListProps, - 'value' | 'onUpdate' | 'items' | 'cantainerRef' | 'size' | 'mapItemDataToProps' + 'value' | 'onUpdate' | 'items' | 'cantainerRef' | 'size' | 'mapItemDataToContentProps' > { itemsCount?: number; } -const mapCustomDataStructureToKnownProps = (props: CustomDataStructure): ListItemCommonProps => ({ +const mapCustomDataStructureToKnownProps = ( + props: CustomDataStructure, +): ListItemViewContentType => ({ title: props.a, }); @@ -61,28 +63,26 @@ export const WithGroupSelectionAndCustomIconStory = ({ {...props} list={list} size="l" - mapItemDataToProps={mapCustomDataStructureToKnownProps} + mapItemDataToContentProps={mapCustomDataStructureToKnownProps} onItemClick={onItemClick} - renderItem={({ - data, - props: { - expanded, // don't use default ListItemView expand icon - ...preparedProps - }, - context: {childrenIds}, - }) => { + renderItem={({id, props: itemProps, context: {childrenIds}}) => { // has no group - preparedProps.hasSelectionIcon = Boolean(props.multiple); + itemProps.selectionViewType = props.multiple ? 'multiple' : 'single'; return ( - } - endSlot={ - childrenIds ? ( + {...itemProps} + content={{ + ...itemProps.content, + isGroup: false, + startSlot: ( + + ), + + endSlot: childrenIds ? ( - ) : undefined - } + ) : undefined, + }} /> ); }} diff --git a/src/components/TreeList/__stories__/stories/WithItemLinksAndActionsStory.tsx b/src/components/TreeList/__stories__/stories/WithItemLinksAndActionsStory.tsx index 261371afa9..58fb2fad2a 100644 --- a/src/components/TreeList/__stories__/stories/WithItemLinksAndActionsStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithItemLinksAndActionsStory.tsx @@ -21,7 +21,7 @@ function identity(value: T): T { } export interface WithItemLinksAndActionsStoryProps - extends Omit, 'items' | 'size' | 'mapItemDataToProps'> {} + extends Omit, 'items' | 'size' | 'mapItemDataToContentProps'> {} export const WithItemLinksAndActionsStory = (props: WithItemLinksAndActionsStoryProps) => { const items = React.useMemo(() => createRandomizedData({num: 10, depth: 1}), []); @@ -43,49 +43,41 @@ export const WithItemLinksAndActionsStory = (props: WithItemLinksAndActionsStory { + renderItem={({id, props: itemProps, context: {childrenIds}}) => { return ( // eslint-disable-next-line jsx-a11y/anchor-is-valid { - e.stopPropagation(); - e.preventDefault(); - }} - items={[ - { - action: (e) => { - e.stopPropagation(); - console.log( - `Clicked by action with id: ${state.id}`, - ); + {...itemProps} + content={{ + ...itemProps.content, + isGroup: false, + endSlot: ( + { + e.stopPropagation(); + e.preventDefault(); + }} + items={[ + { + action: (e) => { + e.stopPropagation(); + console.log(`Clicked by action with id: ${id}`); + }, + text: 'action 1', }, - text: 'action 1', - }, - ]} - defaultSwitcherProps={{ - extraProps: { - 'aria-label': moreOptionsButton, - }, - }} - /> - } - startSlot={ - childrenIds ? ( + ]} + defaultSwitcherProps={{ + extraProps: { + 'aria-label': moreOptionsButton, + }, + }} + /> + ), + startSlot: childrenIds ? ( ) : ( 0 ? {ml: 1} : undefined} + spacing={ + (itemProps.content.indentation || 0) > 0 + ? {ml: 1} + : undefined + } > - ) - } + ), + }} /> ); diff --git a/src/components/TreeList/types.ts b/src/components/TreeList/types.ts index 9dbb33aa22..d74241d5c3 100644 --- a/src/components/TreeList/types.ts +++ b/src/components/TreeList/types.ts @@ -3,11 +3,11 @@ import type React from 'react'; import type {QAProps} from '../types'; import type { ListContainerProps, - ListItemCommonProps, ListItemId, ListItemListContextProps, ListItemSize, - RenderItemProps, + ListItemViewCommonProps, + ListItemViewContentType, UseListResult, } from '../useList'; @@ -15,7 +15,7 @@ export type TreeListRenderItem = (props: { id: ListItemId; data: T; // required item props to render - props: RenderItemProps; + props: ListItemViewCommonProps; // internal list context props context: ListItemListContextProps; list: UseListResult; @@ -29,7 +29,7 @@ export type TreeListContainerProps = ListContainerProps & { export type TreeListRenderContainer = (props: TreeListContainerProps) => React.JSX.Element; -export type TreeListMapItemDataToProps = (item: T) => ListItemCommonProps; +export type TreeListMapItemDataToContentProps = (item: T) => ListItemViewContentType; export type TreeListOnItemClickPayload = {id: ListItemId; list: UseListResult}; @@ -57,5 +57,8 @@ export interface TreeListProps extends QAProps { * `null` - disable default click handler */ onItemClick?: null | TreeListOnItemClick; - mapItemDataToProps: TreeListMapItemDataToProps; + /** + * List item `data` to ListItemView `content` props + */ + mapItemDataToContentProps: TreeListMapItemDataToContentProps; } diff --git a/src/components/TreeSelect/TreeSelect.tsx b/src/components/TreeSelect/TreeSelect.tsx index 73084f2ebd..01dafa08b0 100644 --- a/src/components/TreeSelect/TreeSelect.tsx +++ b/src/components/TreeSelect/TreeSelect.tsx @@ -57,7 +57,7 @@ export const TreeSelect = React.forwardRef(function TreeSelect( renderControl, renderItem = defaultItemRenderer as TreeListRenderItem, renderContainer, - mapItemDataToProps, + mapItemDataToContentProps, onFocus, onBlur, getItemId, @@ -173,7 +173,11 @@ export const TreeSelect = React.forwardRef(function TreeSelect( mapItemDataToProps(list.structure.itemsById[itemId]).title), + value.map((itemId) => + itemId in list.structure.itemsById + ? mapItemDataToContentProps(list.structure.itemsById[itemId]).title + : '', + ), ).join(', ')} view="normal" pin="round-round" @@ -224,7 +228,7 @@ export const TreeSelect = React.forwardRef(function TreeSelect( containerRef={containerRef} onItemClick={handleItemClick} renderContainer={renderContainer} - mapItemDataToProps={mapItemDataToProps} + mapItemDataToContentProps={mapItemDataToContentProps} renderItem={renderItem ?? defaultItemRenderer} /> diff --git a/src/components/TreeSelect/__stories__/Docs.mdx b/src/components/TreeSelect/__stories__/Docs.mdx new file mode 100644 index 0000000000..b4a9f2dc19 --- /dev/null +++ b/src/components/TreeSelect/__stories__/Docs.mdx @@ -0,0 +1,7 @@ +import {Meta, Markdown} from '@storybook/addon-docs'; + +import TreeSelectDocs from './TreeSelectDocs.md?raw'; + + + +{TreeSelectDocs} diff --git a/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx b/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx index 5c5c25035f..a2f20a9d13 100644 --- a/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx +++ b/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx @@ -42,7 +42,7 @@ export default { const DefaultTemplate: StoryFn< Omit< TreeSelectProps<{title: string}>, - 'value' | 'onUpdate' | 'items' | 'mapItemDataToProps' + 'value' | 'onUpdate' | 'items' | 'mapItemDataToContentProps' > & { itemsCount?: number; } @@ -55,7 +55,7 @@ const DefaultTemplate: StoryFn< {...props} placeholder="-" items={items} - mapItemDataToProps={(x) => x} + mapItemDataToContentProps={(x) => x} onItemClick={({id, list}) => { getListItemClickHandler({list})({id}); console.log('clicked on item with id: ', id); diff --git a/src/components/TreeSelect/__stories__/TreeSelectDocs.md b/src/components/TreeSelect/__stories__/TreeSelectDocs.md new file mode 100644 index 0000000000..1977b6d86e --- /dev/null +++ b/src/components/TreeSelect/__stories__/TreeSelectDocs.md @@ -0,0 +1,61 @@ +# TreeList + +Rewritten component [Select](https://preview.gravity-ui.com/uikit/?path=/docs/components-inputs-select--docs) without feature-specific logic using TreeList inside. + +`Storybook` provides complex examples how to use this components from this documentation. + +### Import: + +```tsx +import {unstable_TreeSelect as TreeSelect} from '@gravity-ui/uikit/unstable'; +``` + +### Basic example: + +```tsx +import { + type unstable_ListItemType as ListItemType, + unstable_TreeSelect as TreeSelect, +} from '@gravity-ui/uikit/unstable'; + +const items: ListItemType[] = ['one', 'two', 'free', 'four', 'five']; + + ({title: item})} />; +``` + +### With full list item view override: + +```tsx +import {Text} from '@gravity-ui/uikit'; +import { + type unstable_ListItemType as ListItemType, + type unstable_ListItemView as ListItemView, + unstable_TreeSelect as TreeSelect, +} from '@gravity-ui/uikit/unstable'; + +interface Entity { + id: string; + title: string; +} + +const items: ListItemType[] = [ + {title: 'one', id: '1'}, + {title: 'two', id: '2'}, + {title: 'free', id: '3'}, + {title: 'four', id: '4'}, + {title: 'five', id: '5'}, +]; + +const Component = () => { + return ( + id} + mapItemDataToContentProps={({title}) => ({title})} + renderItem={({data: {title}, props}) => ( + {title}} /> + )} + /> + ); +}; +``` diff --git a/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx b/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx index 0e868891ff..4695224a0b 100644 --- a/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx +++ b/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx @@ -23,7 +23,7 @@ function identity(value: T): T { export interface InfinityScrollExampleProps extends Omit< TreeSelectProps, - 'value' | 'onUpdate' | 'items' | 'mapItemDataToProps' | 'multiple' | 'defaultValue' + 'value' | 'onUpdate' | 'items' | 'mapItemDataToContentProps' | 'multiple' | 'defaultValue' > { itemsCount?: number; } @@ -75,16 +75,20 @@ export const InfinityScrollExample = ({ { + renderItem={({props, context: {isLastItem, childrenIds}}) => { const node = ( {childrenIds.length} + ) : undefined, + }} className={sp({mx: 1})} - endSlot={childrenIds ? : undefined} /> ); diff --git a/src/components/TreeSelect/__stories__/components/WithDisabledElementsExample.tsx b/src/components/TreeSelect/__stories__/components/WithDisabledElementsExample.tsx index 38c991e6ae..fdb31b7530 100644 --- a/src/components/TreeSelect/__stories__/components/WithDisabledElementsExample.tsx +++ b/src/components/TreeSelect/__stories__/components/WithDisabledElementsExample.tsx @@ -10,7 +10,7 @@ interface Entity { } export interface WithDisabledElementsExampleProps - extends Omit, 'items' | 'mapItemDataToProps'> {} + extends Omit, 'items' | 'mapItemDataToContentProps'> {} const items: ListItemType[] = [ { @@ -42,7 +42,7 @@ export const WithDisabledElementsExample = ({...props}: WithDisabledElementsExam items={items} getItemId={({id}) => id} containerRef={containerRef} - mapItemDataToProps={({text}) => ({title: text})} + mapItemDataToContentProps={({text}) => ({title: text})} /> ); }; diff --git a/src/components/TreeSelect/__stories__/components/WithDndListExample.tsx b/src/components/TreeSelect/__stories__/components/WithDndListExample.tsx index 09ef3ffb8b..39b1c9d821 100644 --- a/src/components/TreeSelect/__stories__/components/WithDndListExample.tsx +++ b/src/components/TreeSelect/__stories__/components/WithDndListExample.tsx @@ -38,7 +38,7 @@ type CustomDataType = {someRandomKey: string; id: string}; export interface WithDndListExampleProps extends Omit< TreeSelectProps, - 'value' | 'onUpdate' | 'items' | 'mapItemDataToProps' + 'value' | 'onUpdate' | 'items' | 'mapItemDataToContentProps' > {} const randomItems: CustomDataType[] = createRandomizedData({ @@ -108,10 +108,13 @@ export const WithDndListExample = (storyProps: WithDndListExampleProps) => { index, renderContainerProps, }) => { - const commonProps = { + const commonProps: ListItemViewProps = { ...props, - title: data.someRandomKey, - endSlot: , + content: { + ...props.content, + title: data.someRandomKey, + endSlot: , + }, }; // here passed props from `renderContainer` method. @@ -144,7 +147,7 @@ export const WithDndListExample = (storyProps: WithDndListExampleProps) => { items={items} // you can omit this prop here. If prop `id` passed, TreeSelect would take it by default getItemId={({id}) => id} - mapItemDataToProps={({someRandomKey}) => ({ + mapItemDataToContentProps={({someRandomKey}) => ({ title: someRandomKey, })} renderContainer={renderContainer} diff --git a/src/components/TreeSelect/__stories__/components/WithFiltrationAndControlsExample.tsx b/src/components/TreeSelect/__stories__/components/WithFiltrationAndControlsExample.tsx index 9e7348f234..add573d653 100644 --- a/src/components/TreeSelect/__stories__/components/WithFiltrationAndControlsExample.tsx +++ b/src/components/TreeSelect/__stories__/components/WithFiltrationAndControlsExample.tsx @@ -17,7 +17,7 @@ function identity(value: T): T { export interface WithFiltrationAndControlsExampleProps extends Omit< TreeSelectProps<{title: string}>, - 'value' | 'onUpdate' | 'items' | 'mapItemDataToProps' | 'defaultValue' | 'multiple' + 'value' | 'onUpdate' | 'items' | 'mapItemDataToContentProps' | 'defaultValue' | 'multiple' > { itemsCount?: number; } @@ -51,7 +51,7 @@ export const WithFiltrationAndControlsExample = ({ } - renderItem={({props, data}) => ( + renderItem={({props}) => (
- +
)} renderContainer={renderContainer} diff --git a/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx b/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx index b7229d7b98..0518f155dc 100644 --- a/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx +++ b/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx @@ -6,7 +6,7 @@ import {Button} from '../../../Button'; import {Icon} from '../../../Icon'; import {Flex, spacing} from '../../../layout'; import {ListItemView} from '../../../useList'; -import type {ListItemCommonProps, ListItemId, UseListResult} from '../../../useList'; +import type {ListItemId, ListItemViewContentType, UseListResult} from '../../../useList'; import {createRandomizedData} from '../../../useList/__stories__/utils/makeData'; import {TreeSelect} from '../../TreeSelect'; import type {TreeSelectProps} from '../../types'; @@ -21,18 +21,20 @@ interface CustomDataStructure { export interface WithGroupSelectionControlledStateAndCustomIconExampleProps extends Omit< TreeSelectProps, - 'value' | 'onUpdate' | 'items' | 'mapItemDataToProps' | 'size' + 'value' | 'onUpdate' | 'items' | 'mapItemDataToContentProps' | 'size' > { itemsCount?: number; } -const mapCustomDataStructureToKnownProps = (props: CustomDataStructure): ListItemCommonProps => ({ +const mapCustomDataStructureToKnownProps = ( + props: CustomDataStructure, +): ListItemViewContentType => ({ title: props.a, }); export const WithGroupSelectionControlledStateAndCustomIconExample = ({ itemsCount = 5, - ...props + ...storyProps }: WithGroupSelectionControlledStateAndCustomIconExampleProps) => { // const [value, setValue] = React.useState([]); const [open, setOpen] = React.useState(true); @@ -55,35 +57,30 @@ export const WithGroupSelectionControlledStateAndCustomIconExample = ({ return ( { + renderItem={({id, props, context: {childrenIds}, list}) => { // groups items are selectable too - renderProps.hasSelectionIcon = Boolean(props.multiple); + props.selectionViewType = storyProps.multiple ? 'multiple' : 'single'; return ( - } - endSlot={ - childrenIds ? ( + {...props} + content={{ + ...props.content, + isGroup: false, + startSlot: ( + + ), + endSlot: childrenIds ? ( - ) : undefined - } + ) : undefined, + }} /> ); }} diff --git a/src/components/TreeSelect/__stories__/components/WithItemLinksAndActionsExample.tsx b/src/components/TreeSelect/__stories__/components/WithItemLinksAndActionsExample.tsx index 5fa4174397..c649526331 100644 --- a/src/components/TreeSelect/__stories__/components/WithItemLinksAndActionsExample.tsx +++ b/src/components/TreeSelect/__stories__/components/WithItemLinksAndActionsExample.tsx @@ -18,10 +18,16 @@ function identity(value: T): T { export interface WithItemLinksAndActionsExampleProps extends Omit< TreeSelectProps<{title: string}>, - 'value' | 'onUpdate' | 'items' | 'mapItemDataToProps' | 'size' | 'open' | 'onOpenChange' + | 'value' + | 'onUpdate' + | 'items' + | 'mapItemDataToContentProps' + | 'size' + | 'open' + | 'onOpenChange' > {} -export const WithItemLinksAndActionsExample = (props: WithItemLinksAndActionsExampleProps) => { +export const WithItemLinksAndActionsExample = (storyProps: WithItemLinksAndActionsExampleProps) => { const [value, setValue] = React.useState([]); const [open, setOpen] = React.useState(true); const items = React.useMemo(() => createRandomizedData({num: 10, depth: 1}), []); @@ -39,22 +45,14 @@ export const WithItemLinksAndActionsExample = (props: WithItemLinksAndActionsExa return ( { + renderItem={({id, props, context: {childrenIds}, list}) => { return ( // eslint-disable-next-line jsx-a11y/anchor-is-valid onItemClick(state.id, list)} - endSlot={ - { - e.stopPropagation(); - e.preventDefault(); - }} - items={[ - { - action: (e) => { - e.stopPropagation(); - console.log( - `Clicked by action with id: ${state.id}`, - ); + {...props} + content={{ + ...props.content, + isGroup: false, + endSlot: ( + { + e.stopPropagation(); + e.preventDefault(); + }} + items={[ + { + action: (e) => { + e.stopPropagation(); + console.log( + `Clicked by action with id: ${id}`, + ); + }, + text: 'action 1', }, - text: 'action 1', - }, - ]} - /> - } - startSlot={ - childrenIds ? ( + ]} + /> + ), + startSlot: childrenIds ? ( @@ -108,12 +108,17 @@ export const WithItemLinksAndActionsExample = (props: WithItemLinksAndActionsExa 0 ? {ml: 1} : undefined} + spacing={ + (props.content.indentation ?? 0) > 0 + ? {ml: 1} + : undefined + } > - ) - } + ), + }} + onClick={() => onItemClick(id, list)} /> ); diff --git a/src/components/lab/Breadcrumbs/Breadcrumbs.tsx b/src/components/lab/Breadcrumbs/Breadcrumbs.tsx index cc062d957c..8cd6ef2120 100644 --- a/src/components/lab/Breadcrumbs/Breadcrumbs.tsx +++ b/src/components/lab/Breadcrumbs/Breadcrumbs.tsx @@ -30,7 +30,7 @@ export interface BreadcrumbsItemProps { routerOptions?: RouterOptions; } -function Item(_props: BreadcrumbsItemProps): React.ReactNode { +function Item(_props: BreadcrumbsItemProps): React.ReactElement | null { return null; } diff --git a/src/components/useList/__stories__/Docs.mdx b/src/components/useList/__stories__/Docs.mdx index a7a6ee20ff..2416c5919c 100644 --- a/src/components/useList/__stories__/Docs.mdx +++ b/src/components/useList/__stories__/Docs.mdx @@ -74,7 +74,7 @@ function List() { {list.structure.items.map((_, i) => { const {props} = getItemRenderState({ id: String(i), - mapItemDataToProps: (title) => ({title}), + mapItemDataToContentProps: (title) => ({title}), onItemClick, list, }); @@ -114,7 +114,7 @@ function List() { {(id) => { const {props} = getItemRenderState({ id: String(i), - mapItemDataToProps: (title) => ({title}), + mapItemDataToContentProps: (title) => ({title}), onItemClick, list, }); diff --git a/src/components/useList/__stories__/components/FlattenList.tsx b/src/components/useList/__stories__/components/FlattenList.tsx index 932e6b4201..ddb2e9d501 100644 --- a/src/components/useList/__stories__/components/FlattenList.tsx +++ b/src/components/useList/__stories__/components/FlattenList.tsx @@ -72,10 +72,10 @@ export const FlattenList = ({itemsCount, size}: FlattenListProps) => { id, size, onItemClick, - mapItemDataToProps: (x) => x, + mapItemDataToContentProps: (x) => x, list, }); - return ; + return ; }} diff --git a/src/components/useList/__stories__/components/InfinityScrollList.tsx b/src/components/useList/__stories__/components/InfinityScrollList.tsx index dfbc1adc55..0fb25c3a54 100644 --- a/src/components/useList/__stories__/components/InfinityScrollList.tsx +++ b/src/components/useList/__stories__/components/InfinityScrollList.tsx @@ -72,7 +72,7 @@ export const InfinityScrollList = ({size}: InfinityScrollListProps) => { size, onItemClick, multiple: true, - mapItemDataToProps: (x) => x, + mapItemDataToContentProps: (x) => x, list, }); const node = ; diff --git a/src/components/useList/__stories__/components/ListWithDnd.tsx b/src/components/useList/__stories__/components/ListWithDnd.tsx index 1ffde3968d..1b21848011 100644 --- a/src/components/useList/__stories__/components/ListWithDnd.tsx +++ b/src/components/useList/__stories__/components/ListWithDnd.tsx @@ -77,7 +77,7 @@ export const ListWithDnd = ({size, itemsCount, 'aria-label': ariaLabel}: ListWit id, size, onItemClick, - mapItemDataToProps: (x) => x, + mapItemDataToContentProps: (x) => x, list, }); @@ -93,11 +93,14 @@ export const ListWithDnd = ({size, itemsCount, 'aria-label': ariaLabel}: ListWit ) => ( , + }} {...provided.draggableProps} {...provided.dragHandleProps} dragging={snapshot.isDragging} ref={provided.innerRef} - endSlot={} role="option" /> )} diff --git a/src/components/useList/__stories__/components/PopupWithTogglerList.tsx b/src/components/useList/__stories__/components/PopupWithTogglerList.tsx index 2a835a7969..a96422f85c 100644 --- a/src/components/useList/__stories__/components/PopupWithTogglerList.tsx +++ b/src/components/useList/__stories__/components/PopupWithTogglerList.tsx @@ -83,11 +83,16 @@ export const PopupWithTogglerList = ({size, itemsCount}: PopupWithTogglerListPro id, size, onItemClick, - mapItemDataToProps: (x) => x, + mapItemDataToContentProps: (x) => x, list, }); - return ; + return ( + + ); }} /> diff --git a/src/components/useList/__stories__/components/RecursiveList.tsx b/src/components/useList/__stories__/components/RecursiveList.tsx index 7a1e1ef61b..fd33c4000b 100644 --- a/src/components/useList/__stories__/components/RecursiveList.tsx +++ b/src/components/useList/__stories__/components/RecursiveList.tsx @@ -58,11 +58,16 @@ export const RecursiveList = ({size, itemsCount, 'aria-label': ariaLabel}: Recur size, onItemClick, multiple: true, - mapItemDataToProps: (x) => x, + mapItemDataToContentProps: (x) => x, list, }); - return ; + return ( + + ); }} /> diff --git a/src/components/useList/__stories__/docs/get-item-render-state.md b/src/components/useList/__stories__/docs/get-item-render-state.md index da4a87325c..99f5e954b1 100644 --- a/src/components/useList/__stories__/docs/get-item-render-state.md +++ b/src/components/useList/__stories__/docs/get-item-render-state.md @@ -19,7 +19,7 @@ const {data, props, context} = getItemRenderState({ multiple: true, size, // list size onItemClick, - mapItemDataToProps: (item) => ({title: item.title}), + mapItemDataToContentProps: (item) => ({title: item.title}), list, }); @@ -28,16 +28,16 @@ return ; #### Props: -| Name | Description | Type | Default | -| :----------------- | :--------------------------------------------------------------------------------- | :------------------------------------------------------------: | :-----: | -| id | `id` of list item | `ListItemId` | | -| list | result of `useList` hook | `UseListResult` | | -| multiple | One or multiple elements selected list | `boolean` | | -| onItemClick | Optional on click handler | `(payload :{id: ListItemId}, e: React.SyntheticEvent) => void` | | -| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | -| mapItemDataToProps | Map list item data (`T`) to `ListItemView` props | `(data: T) => ListItemCommonProps` | | +| Name | Description | Type | Default | +| :------------------------ | :--------------------------------------------------------------------------------- | :------------------------------------------------------------: | :-----: | +| id | `id` of list item | `ListItemId` | | +| list | result of `useList` hook | `UseListResult` | | +| multiple | One or multiple elements selected list | `boolean` | | +| onItemClick | Optional on click handler | `(payload :{id: ListItemId}, e: React.SyntheticEvent) => void` | | +| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | +| mapItemDataToContentProps | Map list item data (`T`) to `ListItemView` `content` prop | `(data: T) => ListItemViewContentProps` | | -##### ListItemCommonProps +##### ListItemViewContentProps | Name | Type | Note | | :-------- | :---------------: | :------: | @@ -83,7 +83,7 @@ const onItemClick = () => {}; multiple: false, size, // list size onItemClick, - mapItemDataToProps, + mapItemDataToContentProps, list, }); diff --git a/src/components/useList/__stories__/docs/list-container-view.md b/src/components/useList/__stories__/docs/list-container-view.md index 46285a926b..49844445d9 100644 --- a/src/components/useList/__stories__/docs/list-container-view.md +++ b/src/components/useList/__stories__/docs/list-container-view.md @@ -17,7 +17,7 @@ The default container for all custom lists. Contains all html attributes and sty const containerRef = React.useRef(null); - - + + ; ``` diff --git a/src/components/useList/__stories__/docs/list-item-view.md b/src/components/useList/__stories__/docs/list-item-view.md index d0b6bbbe70..95a8ae931f 100644 --- a/src/components/useList/__stories__/docs/list-item-view.md +++ b/src/components/useList/__stories__/docs/list-item-view.md @@ -30,9 +30,12 @@ const List = () => { } /> ) }} @@ -43,24 +46,31 @@ const List = () => { #### Props: +| Name | Description | Type | Default | +| :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :-------------------------------: | :-----: | +| id | Required prop. Set `[data-list-item="${id}"]` data attribute. By this it core list engine finds elements to scroll to. | `string` | | +| as | If needed, override `html` tag. By default - `li` | `HTMLElement` | `li` | +| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | +| height | The height of the element in pixels. By default, it is calculated depending on the `size` parameter and the presence of the `subtitle` parameter.
Also you can define item height by two variants:
- component props `height`;
- css custom property `--g-list-item-height`; | `number ` | | +| selected | The selected state of the component | `boolean ` | | +| active | The state when the element is in the user's focus, but not selected. It can also be used when you drag an element | `boolean ` | | +| disabled | The disabled state. It also prevents clicking on an element | `boolean ` | | +| activeOnHover | directly control hover behavior | `boolean ` | | +| onClick | On item click callback. If `disabled` option is `true` click don't appears | `() => void` | | +| content | Typed props or ReactNode in difficult cases | `ContentProps \| React.ReactNode` | | + +#### ContentProps + | Name | Description | Type | Default | | :--------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------: | :-----: | -| id | Required prop. Set `[data-list-item="${id}"]` data attribute. By this it core list engine finds elements to scroll to. | `string` | | | title | Base required prop to use. If passed string, applies default component styles according design system. Pass you own component if you wont custom behavior; | `React.ReactNode` | | | subtitle | Slot under `title`. If passed string apply predefined styles. Or you can pass custom `React.ReactNode` to use you own behavior | `React.ReactNode` | | -| as | If needed, override `html` tag. By default - `li` | `HTMLElement` | `li` | -| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | -| height | The height of the element in pixels. By default, it is calculated depending on the `size` parameter and the presence of the `subtitle` parameter | `number ` | | -| selected | The selected state of the component | `boolean ` | | -| active | The state when the element is in the user's focus, but not selected. It can also be used when you drag an element | `boolean ` | | -| disabled | The disabled state. It also prevents clicking on an element | `boolean ` | | -| activeOnHover | directly control hover behavior | `boolean ` | | -| indentation | Affects the visual indentation of the element content | `number ` | | -| hasSelectionIcon | Show selected icon if selected and reserve space for this icon | `boolean ` | | -| onClick | On item click callback. If `disabled` option is `true` click don't appears | `() => void` | | -| startSlot | Custom slot before `title` | `React.ReactNode` | | -| endSlot | Custom slot before `title` | `React.ReactNode` | | | style | Inline styles if needed | `React.CSSProperties` | | | className | Custom class name to mix with | `string` | | -| expanded | Adds a visual representation of a group element if the value is different from `undefined` | `string \| undefined` | | | dragging | manage view of dragging element. Required for draggable list implementation | `boolean` | | +| indentation | Affects the visual indentation of the element content | `number ` | | +| hasSelectionIcon | Show selected icon if selected and reserve space for this icon | `boolean ` | | +| isGroup | Applies group styles view to list element | `boolean` | | +| expanded | Control group item view state expended/closed | `string \| undefined` | | +| startSlot | Custom slot before `title` | `React.ReactNode` | | +| endSlot | Custom slot after `title` | `React.ReactNode` | | diff --git a/src/components/useList/__stories__/docs/list-recursive-renderer.md b/src/components/useList/__stories__/docs/list-recursive-renderer.md index 71558379e0..fc5d29b61f 100644 --- a/src/components/useList/__stories__/docs/list-recursive-renderer.md +++ b/src/components/useList/__stories__/docs/list-recursive-renderer.md @@ -49,7 +49,7 @@ function List() { {(id) => { const {props} = getItemRenderState({ id: String(i), - mapItemDataToProps: (title) => ({title}), + mapItemDataToContentProps: (title) => ({title}), onItemClick, list, }); diff --git a/src/components/useList/components/ListItemView/ListItemView.scss b/src/components/useList/components/ListItemView/ListItemView.scss index b589561020..9eb47b09d9 100644 --- a/src/components/useList/components/ListItemView/ListItemView.scss +++ b/src/components/useList/components/ListItemView/ListItemView.scss @@ -4,6 +4,14 @@ $block: '.#{variables.$ns}list-item-view'; #{$block} { flex-shrink: 0; + display: flex; + flex-grow: 1; + align-items: center; + + &__content { + width: 100%; + height: 100%; + } &__main-content { width: 100%; diff --git a/src/components/useList/components/ListItemView/ListItemView.tsx b/src/components/useList/components/ListItemView/ListItemView.tsx index 83caf482ff..8250e1212d 100644 --- a/src/components/useList/components/ListItemView/ListItemView.tsx +++ b/src/components/useList/components/ListItemView/ListItemView.tsx @@ -1,64 +1,53 @@ import React from 'react'; -import {Check, ChevronDown, ChevronUp} from '@gravity-ui/icons'; - -import {Icon} from '../../../Icon'; -import {Text, colorText} from '../../../Text'; -import {Flex, spacing} from '../../../layout'; -import type {FlexProps} from '../../../layout'; +import {spacing} from '../../../layout'; import type {QAProps} from '../../../types'; -import {block} from '../../../utils/cn'; import {LIST_ITEM_DATA_ATR, modToHeight} from '../../constants'; -import type {ListItemCommonProps, ListItemId, ListItemSize} from '../../types'; - -import './ListItemView.scss'; +import type {ListItemId, ListItemSize, ListItemViewContentType} from '../../types'; -const b = block('list-item-view'); +import {ListItemViewContent, isListItemContentPropsGuard} from './ListItemViewContent'; +import {b} from './styles'; -export interface ListItemViewProps - extends QAProps, - ListItemCommonProps { - /** - * Ability to override default html tag - */ - as?: T; +export interface ListItemViewCommonProps extends QAProps { /** * @default `m` */ size?: ListItemSize; - height?: number; - selected?: boolean; - active?: boolean; - disabled?: boolean; /** - * By default hovered elements has active styles. You can disable this behavior + * `[${LIST_ITEM_DATA_ATR}="${id}"]` data attribute to find element. + * For example for scroll to */ - activeOnHover?: boolean; + id: ListItemId; /** - * Build in indentation component to render nested views structure + * Note: if passed and `disabled` option is `true` click will not be appear */ - indentation?: number; + onClick?: React.ComponentPropsWithoutRef['onClick']; + selected?: boolean; + disabled?: boolean; + active?: boolean; + selectionViewType?: 'single' | 'multiple'; + content: ListItemViewContentType; +} + +export interface ListItemViewProps + extends Omit { /** - * Show selected icon if selected and reserve space for this icon + * Ability to override default html tag */ - hasSelectionIcon?: boolean; + as?: T; + height?: number; /** - * Note: if passed and `disabled` option is `true` click will not be appear + * By default hovered elements has active styles. You can disable this behavior */ - onClick?: React.ComponentPropsWithoutRef['onClick']; + activeOnHover?: boolean; style?: React.CSSProperties; className?: string; role?: React.AriaRole; - expanded?: boolean; /** * Add active styles and change selection behavior during dnd is performing */ dragging?: boolean; - /** - * `[${LIST_ITEM_DATA_ATR}="${id}"]` data attribute to find element. - * For example for scroll to - */ - id: ListItemId; + content: ListItemViewContentType | React.ReactNode; } type ListItemViewRef = React.ComponentPropsWithRef['ref']; @@ -66,32 +55,6 @@ type ListItemViewRef = React.ComponentPropsWithRef< type ListItemViewPropsWithTypedAttrs = ListItemViewProps & Omit, keyof ListItemViewProps>; -interface SlotProps extends FlexProps { - indentation?: number; -} - -export const ListItemViewSlot = ({ - children, - indentation: indent = 1, - className, - ...props -}: SlotProps) => { - return ( - - {children} - - ); -}; - -const renderSafeIndentation = (indentation?: number) => { - if (indentation && indentation >= 1) { - return ( - - ); - } - return null; -}; - export const ListItemView = React.forwardRef(function ListItemView< T extends React.ElementType = 'li', >( @@ -102,32 +65,35 @@ export const ListItemView = React.forwardRef(function ListItemView< active, selected, disabled, + selectionViewType = 'multiple', activeOnHover: propsActiveOnHover, className, - hasSelectionIcon = true, - indentation, - startSlot, - subtitle, - endSlot, - title, height, - expanded, dragging, - style, + style: propsStyle, + content, role = 'option', onClick: _onClick, ...rest - }: ListItemViewPropsWithTypedAttrs, + }: ListItemViewProps, ref?: ListItemViewRef, ) { - const as: React.ElementType = asProps || 'li'; - const isGroup = typeof expanded === 'boolean'; + const Tag: React.ElementType = asProps || 'li'; const onClick = disabled ? undefined : _onClick; const activeOnHover = typeof propsActiveOnHover === 'boolean' ? propsActiveOnHover : Boolean(onClick); + const style = { + minHeight: `var(--g-list-item-height, ${ + height ?? + modToHeight[size][ + Number(Boolean(isListItemContentPropsGuard(content) ? content?.subtitle : false)) + ] + }px)`, + ...propsStyle, + }; return ( - - - {hasSelectionIcon && ( - - {selected ? ( - - ) : null} - - )} - - {renderSafeIndentation(indentation)} - - {isGroup ? ( - - ) : null} - - {startSlot} - -
- {typeof title === 'string' ? ( - - {title} - - ) : ( - title - )} - {typeof subtitle === 'string' ? ( - - {subtitle} - - ) : ( - subtitle - )} -
-
- - {endSlot} -
+ {isListItemContentPropsGuard(content) ? ( + + ) : ( + content + )} + ); }) as ({ ref, diff --git a/src/components/useList/components/ListItemView/ListItemViewContent.tsx b/src/components/useList/components/ListItemView/ListItemViewContent.tsx new file mode 100644 index 0000000000..571ac29f61 --- /dev/null +++ b/src/components/useList/components/ListItemView/ListItemViewContent.tsx @@ -0,0 +1,110 @@ +import React from 'react'; + +import {Check, ChevronDown, ChevronUp} from '@gravity-ui/icons'; + +import {Icon} from '../../../Icon'; +import {Text, colorText} from '../../../Text'; +import {Flex} from '../../../layout'; +import type {FlexProps} from '../../../layout'; +import type {ListItemViewContentType} from '../../types'; + +import {b} from './styles'; + +export const isListItemContentPropsGuard = ( + props: ListItemViewContentType | React.ReactNode, +): props is ListItemViewContentType => { + return typeof props === 'object' && props !== null && 'title' in props; +}; + +interface SlotProps extends FlexProps { + indentation?: number; +} + +const ListItemViewSlot = ({children, indentation = 1, className, ...props}: SlotProps) => { + return ( + + {children} + + ); +}; + +const renderSafeIndentation = (indentation?: number) => { + if (indentation && indentation >= 1) { + return ( + + ); + } + return null; +}; + +interface ListItemViewContentProps extends ListItemViewContentType { + selected?: boolean; + disabled?: boolean; + /** + * Show selected icon if selected and reserve space for this icon + */ + hasSelectionIcon: boolean; +} + +export const ListItemViewContent = ({ + startSlot, + subtitle, + endSlot, + disabled, + hasSelectionIcon, + isGroup, + indentation, + expanded, + selected, + title, +}: ListItemViewContentProps) => { + return ( + + + {hasSelectionIcon && ( + + {selected ? ( + + ) : null} + + )} + + {renderSafeIndentation(indentation)} + + {isGroup ? ( + + ) : null} + + {startSlot} + +
+ {typeof title === 'string' ? ( + + {title} + + ) : ( + title + )} + {typeof subtitle === 'string' ? ( + + {subtitle} + + ) : ( + subtitle + )} +
+
+ + {endSlot} +
+ ); +}; diff --git a/src/components/useList/components/ListItemView/__stories__/ListItemView.stories.tsx b/src/components/useList/components/ListItemView/__stories__/ListItemView.stories.tsx index 2a9d5be1ec..2010745208 100644 --- a/src/components/useList/components/ListItemView/__stories__/ListItemView.stories.tsx +++ b/src/components/useList/components/ListItemView/__stories__/ListItemView.stories.tsx @@ -9,6 +9,7 @@ import {Flex, sp} from '../../../../layout'; import type {ListItemId} from '../../../../useList/types'; import {ListItemView as ListItemViewComponent} from '../ListItemView'; import type {ListItemViewProps} from '../ListItemView'; +import {isListItemContentPropsGuard} from '../ListItemViewContent'; export default { title: 'Lab/useList/ListItemView', @@ -72,128 +73,164 @@ const EndSlot = ({selfStart}: {selfStart?: boolean}) => ( const stories: ListItemViewProps[] = [ { id: '1', - title, - activeOnHover: false, - subtitle, + content: { + title, + subtitle, + startSlot: , + }, disabled: true, - startSlot: , + activeOnHover: false, }, { id: '2', - title, - subtitle: 'activeOnHover - false', + content: { + title, + subtitle: 'activeOnHover - false', + endSlot: , + }, + selected: true, activeOnHover: false, - endSlot: , }, { id: '3', - title, + selectionViewType: 'single', + content: { + title, + subtitle, + startSlot: , + }, + selected: true, size: 'l', - subtitle, - hasSelectionIcon: false, - startSlot: , }, { id: '4', - title, + content: { + title, + startSlot: , + }, disabled: true, size: 'xl', height: 60, - startSlot: , }, { id: '5', size: 'l', - startSlot: , - title, + content: { + startSlot: , + title, + }, }, { id: '6', - title: 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi officia qui deserunt autem quas necessitatibus nam possimus aperiam.', - size: 'l', - subtitle: 'indentation 1', - startSlot: , - indentation: 1, + content: { + title: 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi officia qui deserunt autem quas necessitatibus nam possimus aperiam.', + subtitle: 'indentation 1', + startSlot: , + indentation: 1, + endSlot: , + }, selected: true, - endSlot: , + size: 'l', }, { id: '7', - expanded: true, size: 'xl', - title: 'Group 1', - endSlot: , + content: { + isGroup: true, + expanded: true, + title: 'Group 1', + endSlot: , + }, }, { id: '8', - hasSelectionIcon: false, - expanded: true, + selectionViewType: 'single', + content: { + title: 'Group 1', + expanded: true, + isGroup: true, + }, disabled: true, size: 'xl', - title: 'Group 1', }, { id: '9', - hasSelectionIcon: false, - title: ( - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, - voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi officia - qui deserunt autem quas necessitatibus nam possimus aperiam. - - ), + selectionViewType: 'single', + content: { + subtitle: ( + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, + voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi + officia qui deserunt autem quas necessitatibus nam possimus aperiam. + + ), + startSlot: , + title: ( + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, + voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi + officia qui deserunt autem quas necessitatibus nam possimus aperiam. + + ), + endSlot: , + }, size: 'l', - subtitle: ( - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, - voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi officia - qui deserunt autem quas necessitatibus nam possimus aperiam. - - ), - startSlot: , selected: true, className: sp({p: 2}), - endSlot: , }, { id: '10', - title: ( - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, - voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi officia - qui deserunt autem quas necessitatibus nam possimus aperiam. - - ), - size: 'l', - subtitle: ( - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, - voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi officia - qui deserunt autem quas necessitatibus nam possimus aperiam. - - ), - startSlot: , + content: { + title: ( + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, + voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi + officia qui deserunt autem quas necessitatibus nam possimus aperiam. + + ), + subtitle: ( + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, + voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi + officia qui deserunt autem quas necessitatibus nam possimus aperiam. + + ), + startSlot: , + indentation: 1, + endSlot: , + }, selected: true, - indentation: 1, + size: 'l', className: sp({p: 2}), - endSlot: , }, { id: '11', - title: 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi officia qui deserunt autem quas necessitatibus nam possimus aperiam.', size: 'l', - subtitle: ( - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, - voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi officia - qui deserunt autem quas necessitatibus nam possimus aperiam. - + content: { + title: 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi officia qui deserunt autem quas necessitatibus nam possimus aperiam.', + subtitle: ( + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, + voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi + officia qui deserunt autem quas necessitatibus nam possimus aperiam. + + ), + expanded: true, + isGroup: true, + startSlot: , + indentation: 1, + endSlot: , + }, + }, + { + id: '12', + size: 'l', + content: ( + + + Override list item context with react node + ), - expanded: true, - startSlot: , - indentation: 1, - selected: true, - endSlot: , }, ]; @@ -202,35 +239,55 @@ const ListItemViewTemplate: StoryFn = () => { const [selectedById, setSelectedById] = React.useState>({}); return ( - - {stories.map((props, i) => ( - - ))} + + {stories.map((props, i) => { + let expanded: boolean | undefined; + + if (isListItemContentPropsGuard(props.content) && props.content.isGroup) { + expanded = + props.id in expandedById ? expandedById[props.id] : props.content.expanded; + } + + return ( + + ); + })} ); - function handleClick({id, expanded}: ListItemViewProps) { - const isGroup = typeof expanded === 'boolean'; + function handleClick({id, content}: ListItemViewProps) { + if (isListItemContentPropsGuard(content)) { + const isGroup = content.isGroup; + + return () => { + if (isGroup) { + setExpandedById((prevState) => ({ + ...prevState, + [id]: id in prevState ? !prevState[id] : !content.expanded, + })); + } else { + setSelectedById((prevState) => ({ + ...prevState, + [id]: !prevState[id], + })); + } + }; + } - return () => { - if (isGroup) { - setExpandedById((prevState) => ({ - ...prevState, - [id]: typeof prevState[id] === 'undefined' ? !expanded : !prevState[id], - })); - } else { - setSelectedById((prevState) => ({ - ...prevState, - [id]: !prevState[id], - })); - } - }; + return undefined; } }; export const ListItemView = ListItemViewTemplate.bind({}); diff --git a/src/components/useList/components/ListItemView/index.ts b/src/components/useList/components/ListItemView/index.ts index 8b2f736a59..0df3d97ab0 100644 --- a/src/components/useList/components/ListItemView/index.ts +++ b/src/components/useList/components/ListItemView/index.ts @@ -1,2 +1,3 @@ export {ListItemView} from './ListItemView'; -export type {ListItemViewProps} from './ListItemView'; +export {isListItemContentPropsGuard} from './ListItemViewContent'; +export type {ListItemViewProps, ListItemViewCommonProps} from './ListItemView'; diff --git a/src/components/useList/components/ListItemView/styles.ts b/src/components/useList/components/ListItemView/styles.ts new file mode 100644 index 0000000000..23774472a7 --- /dev/null +++ b/src/components/useList/components/ListItemView/styles.ts @@ -0,0 +1,5 @@ +import {block} from '../../../utils/cn'; + +import './ListItemView.scss'; + +export const b = block('list-item-view'); diff --git a/src/components/useList/hooks/useList.ts b/src/components/useList/hooks/useList.ts index 69000f2a74..2446bf5b06 100644 --- a/src/components/useList/hooks/useList.ts +++ b/src/components/useList/hooks/useList.ts @@ -32,9 +32,9 @@ export const useList = ({ const initValues = React.useMemo(() => { return { - expandedById: {...initialValues?.expandedById, ...initialState.expandedById}, - selectedById: {...initialValues?.selectedById, ...initialState.selectedById}, - disabledById: {...initialValues?.disabledById, ...initialState.disabledById}, + expandedById: {...initialState.expandedById, ...initialValues?.expandedById}, + selectedById: {...initialState.selectedById, ...initialValues?.selectedById}, + disabledById: {...initialState.disabledById, ...initialValues?.disabledById}, }; }, [ initialState.disabledById, diff --git a/src/components/useList/migration-guide.md b/src/components/useList/migration-guide.md new file mode 100644 index 0000000000..7985e60b19 --- /dev/null +++ b/src/components/useList/migration-guide.md @@ -0,0 +1,78 @@ +# 6.23 + +## ListItemView: + +- changed prop `hasSelectionIcon?: boolean` to prop `selectionViewType?: 'multiple' | 'single'` with default value `multiple`; +- now mix of ccs class works that applied at root of component; +- ability to set item height by css custom property `--g-list-item-height`; +- new ability to pass custom react node as `content` prop; + +```diff + +``` + +## TreeList: + +- changed `mapItemDataToProps` prop name to `mapItemDataToContentProps`: + +```diff + + size="l" + list={list} +- mapItemDataToProps={item => ({title: item.text})} ++ mapItemDataToContentProps={item => ({title: item.text})} + multiple={multiple} + renderItem={({props, context: {childrenIds}}) => { + return ( + {childrenIds.length} : undefined} ++ content={{ ++ ...props.content, ++ endSlot: childrenIds ? ( ++ ++ ) : undefined, + }} + /> + ) + })} + /> +``` + +## getItemRenderState: + +- changed `mapItemDataToProps` prop name to `mapItemDataToContentProps`: + +```diff +const {props, context} = getItemRenderState({ + id, + size, + onItemClick, +- mapItemDataToProps: (x) => x, ++ mapItemDataToContentProps: (x) => x, + list, +}); +``` diff --git a/src/components/useList/types.ts b/src/components/useList/types.ts index 5afa809406..6af991e373 100644 --- a/src/components/useList/types.ts +++ b/src/components/useList/types.ts @@ -1,5 +1,3 @@ -import type {QAProps} from '../types'; - export type ListItemId = string; export type ListItemSize = 's' | 'm' | 'l' | 'xl'; @@ -41,11 +39,17 @@ export type ItemState = { indentation: number; }; -export type ListItemCommonProps = { +export type ListItemViewContentType = { title: React.ReactNode; subtitle?: React.ReactNode; startSlot?: React.ReactNode; endSlot?: React.ReactNode; + /** + * Build in indentation component to render nested views structure + */ + indentation?: number; + isGroup?: boolean; + expanded?: boolean; }; export type ListItemListContextProps = ItemState & @@ -53,19 +57,6 @@ export type ListItemListContextProps = ItemState & isLastItem: boolean; }; -export type RenderItemProps = { - size: ListItemSize; - id: ListItemId; - onClick: ((e: React.SyntheticEvent) => void) | undefined; - selected: boolean | undefined; - disabled: boolean; - expanded: boolean | undefined; - active: boolean; - indentation: number; - hasSelectionIcon?: boolean; -} & ListItemCommonProps & - QAProps; - export type ParsedState = { /** * Stored internal meta info about item diff --git a/src/components/useList/utils/getItemRenderState.tsx b/src/components/useList/utils/getItemRenderState.tsx index c66cc05fb6..31aa525412 100644 --- a/src/components/useList/utils/getItemRenderState.tsx +++ b/src/components/useList/utils/getItemRenderState.tsx @@ -1,12 +1,12 @@ /* eslint-disable valid-jsdoc */ import type {QAProps} from '../../types'; +import type {ListItemViewCommonProps} from '../components/ListItemView'; import type { - ListItemCommonProps, ListItemId, ListItemListContextProps, ListItemSize, + ListItemViewContentType, ListOnItemClick, - RenderItemProps, UseListResult, } from '../types'; @@ -19,7 +19,7 @@ type ItemRendererProps = QAProps & { */ multiple?: boolean; id: ListItemId; - mapItemDataToProps(data: T): ListItemCommonProps; + mapItemDataToContentProps(data: T): ListItemViewContentType; onItemClick?: ListOnItemClick; list: UseListResult; }; @@ -31,7 +31,7 @@ export const getItemRenderState = ({ qa, list, onItemClick, - mapItemDataToProps, + mapItemDataToContentProps, size = 'm', multiple = false, id, @@ -43,24 +43,20 @@ export const getItemRenderState = ({ id === list.structure.visibleFlattenIds[list.structure.visibleFlattenIds.length - 1], }; - let expanded; // `undefined` value means than tree list will look as nested list without groups - - // isGroup - if (list.state.expandedById && id in list.state.expandedById) { - expanded = list.state.expandedById[id]; - } - - const props: RenderItemProps = { + const props: ListItemViewCommonProps = { id, size, - expanded, - active: id === list.state.activeItemId, - indentation: context.indentation, - disabled: Boolean(list.state.disabledById?.[id]), selected: Boolean(list.state.selectedById[id]), - hasSelectionIcon: Boolean(multiple) && !context.childrenIds, // hide multiple selection view at group nodes + disabled: Boolean(list.state.disabledById?.[id]), + active: id === list.state.activeItemId, onClick: onItemClick ? (e: React.SyntheticEvent) => onItemClick({id}, e) : undefined, - ...mapItemDataToProps(list.structure.itemsById[id]), + selectionViewType: Boolean(multiple) && !context.childrenIds ? 'multiple' : 'single', // no multiple selection at group nodes + content: { + expanded: list.state.expandedById?.[id], + indentation: context.indentation, + isGroup: list.state.expandedById && id in list.state.expandedById, + ...mapItemDataToContentProps(list.structure.itemsById[id]), + }, }; if (qa) { diff --git a/styles/mixins.scss b/styles/mixins.scss index aaa49761d1..5811f00cd0 100644 --- a/styles/mixins.scss +++ b/styles/mixins.scss @@ -157,135 +157,150 @@ // Typography @mixin text-body-1() { + font-family: var(--g-text-body-font-family); + font-weight: var(--g-text-body-font-weight); font-size: var(--g-text-body-1-font-size); line-height: var(--g-text-body-1-line-height); - font-weight: var(--g-text-body-font-weight); } @mixin text-body-2() { + font-family: var(--g-text-body-font-family); + font-weight: var(--g-text-body-font-weight); font-size: var(--g-text-body-2-font-size); line-height: var(--g-text-body-2-line-height); - font-weight: var(--g-text-body-font-weight); } @mixin text-body-3() { + font-family: var(--g-text-body-font-family); + font-weight: var(--g-text-body-font-weight); font-size: var(--g-text-body-3-font-size); line-height: var(--g-text-body-3-line-height); - font-weight: var(--g-text-body-font-weight); } @mixin text-body-short() { + font-family: var(--g-text-body-font-family); + font-weight: var(--g-text-body-font-weight); font-size: var(--g-text-body-short-font-size); line-height: var(--g-text-body-short-line-height); - font-weight: var(--g-text-body-font-weight); } @mixin text-caption-1() { + font-family: var(--g-text-caption-font-family); + font-weight: var(--g-text-caption-font-weight); font-size: var(--g-text-caption-1-font-size); line-height: var(--g-text-caption-1-line-height); - font-weight: var(--g-text-caption-font-weight); } @mixin text-caption-2() { + font-family: var(--g-text-caption-font-family); + font-weight: var(--g-text-caption-font-weight); font-size: var(--g-text-caption-2-font-size); line-height: var(--g-text-caption-2-line-height); - font-weight: var(--g-text-caption-font-weight); } @mixin text-header-1() { + font-family: var(--g-text-header-font-family); + font-weight: var(--g-text-header-font-weight); font-size: var(--g-text-header-1-font-size); line-height: var(--g-text-header-1-line-height); - font-weight: var(--g-text-header-font-weight); } @mixin text-header-2() { + font-family: var(--g-text-header-font-family); + font-weight: var(--g-text-header-font-weight); font-size: var(--g-text-header-2-font-size); line-height: var(--g-text-header-2-line-height); - font-weight: var(--g-text-header-font-weight); } @mixin text-subheader-1() { + font-family: var(--g-text-subheader-font-family); + font-weight: var(--g-text-subheader-font-weight); font-size: var(--g-text-subheader-1-font-size); line-height: var(--g-text-subheader-1-line-height); - font-weight: var(--g-text-subheader-font-weight); } @mixin text-subheader-2() { + font-family: var(--g-text-subheader-font-family); + font-weight: var(--g-text-subheader-font-weight); font-size: var(--g-text-subheader-2-font-size); line-height: var(--g-text-subheader-2-line-height); - font-weight: var(--g-text-subheader-font-weight); } @mixin text-subheader-3() { + font-family: var(--g-text-subheader-font-family); + font-weight: var(--g-text-subheader-font-weight); font-size: var(--g-text-subheader-3-font-size); line-height: var(--g-text-subheader-3-line-height); - font-weight: var(--g-text-subheader-font-weight); } @mixin text-display-1() { + font-family: var(--g-text-display-font-family); + font-weight: var(--g-text-display-font-weight); font-size: var(--g-text-display-1-font-size); line-height: var(--g-text-display-1-line-height); - font-weight: var(--g-text-display-font-weight); } @mixin text-display-2() { + font-family: var(--g-text-display-font-family); + font-weight: var(--g-text-display-font-weight); font-size: var(--g-text-display-2-font-size); line-height: var(--g-text-display-2-line-height); - font-weight: var(--g-text-display-font-weight); } @mixin text-display-3() { + font-family: var(--g-text-display-font-family); + font-weight: var(--g-text-display-font-weight); font-size: var(--g-text-display-3-font-size); line-height: var(--g-text-display-3-line-height); - font-weight: var(--g-text-display-font-weight); } @mixin text-display-4() { + font-family: var(--g-text-display-font-family); + font-weight: var(--g-text-display-font-weight); font-size: var(--g-text-display-4-font-size); line-height: var(--g-text-display-4-line-height); - font-weight: var(--g-text-display-font-weight); } @mixin text-code-1() { - font-family: var(--g-font-family-monospace); + font-family: var(--g-text-code-font-family); + font-weight: var(--g-text-code-font-weight); font-size: var(--g-text-code-1-font-size); line-height: var(--g-text-code-1-line-height); - font-weight: var(--g-text-code-font-weight); } @mixin text-code-2() { - font-family: var(--g-font-family-monospace); + font-family: var(--g-text-code-font-family); + font-weight: var(--g-text-code-font-weight); font-size: var(--g-text-code-2-font-size); line-height: var(--g-text-code-2-line-height); - font-weight: var(--g-text-code-font-weight); } @mixin text-code-3() { - font-family: var(--g-font-family-monospace); + font-family: var(--g-text-code-font-family); + font-weight: var(--g-text-code-font-weight); font-size: var(--g-text-code-3-font-size); line-height: var(--g-text-code-3-line-height); - font-weight: var(--g-text-code-font-weight); } @mixin text-code-inline-1() { - font-family: var(--g-font-family-monospace); + font-family: var(--g-text-code-font-family); + font-weight: var(--g-text-code-font-weight); font-size: var(--g-text-code-inline-1-font-size); line-height: var(--g-text-code-inline-1-line-height); - font-weight: var(--g-text-code-font-weight); } @mixin text-code-inline-2() { - font-family: var(--g-font-family-monospace); + font-family: var(--g-text-code-font-family); + font-weight: var(--g-text-code-font-weight); font-size: var(--g-text-code-inline-2-font-size); line-height: var(--g-text-code-inline-2-line-height); - font-weight: var(--g-text-code-font-weight); } @mixin text-code-inline-3() { - font-family: var(--g-font-family-monospace); + font-family: var(--g-text-code-font-family); + font-weight: var(--g-text-code-font-weight); font-size: var(--g-text-code-inline-3-font-size); line-height: var(--g-text-code-inline-3-line-height); - font-weight: var(--g-text-code-font-weight); } @mixin text-accent() { diff --git a/styles/themes/common/typography.scss b/styles/themes/common/typography.scss index 1f58ad09e2..de34b16b62 100644 --- a/styles/themes/common/typography.scss +++ b/styles/themes/common/typography.scss @@ -81,8 +81,8 @@ --g-text-code-inline-3-font-size: 16px; --g-text-code-inline-3-line-height: 20px; - font-family: var(--g-font-family-sans); + font-family: var(--g-text-body-font-family); + font-weight: var(--g-text-body-font-weight); font-size: var(--g-text-body-1-font-size); line-height: var(--g-text-body-1-line-height); - font-weight: var(--g-text-body-font-weight); }