From 8de16536b7541835d58cc651a3ab4248f0e1ebf0 Mon Sep 17 00:00:00 2001 From: Denis Vershkov Date: Wed, 17 Apr 2024 15:01:35 +0300 Subject: [PATCH 01/24] fix: add load more functionallity to virtualized list (#1490) (#1513) --- src/components/List/List.tsx | 14 ++-- src/components/List/ListLoadingIndicator.tsx | 4 +- src/components/List/__tests__/List.test.tsx | 68 ++++++++++++++++++++ test-utils/setup-tests.ts | 10 +++ test-utils/setupIntersectionObserverMock.ts | 50 ++++++++++++++ 5 files changed, 136 insertions(+), 10 deletions(-) create mode 100644 src/components/List/__tests__/List.test.tsx create mode 100644 test-utils/setupIntersectionObserverMock.ts diff --git a/src/components/List/List.tsx b/src/components/List/List.tsx index 2e8463085e..b69f8a7539 100644 --- a/src/components/List/List.tsx +++ b/src/components/List/List.tsx @@ -2,7 +2,6 @@ import React from 'react'; import isEqual from 'lodash/isEqual'; import isObject from 'lodash/isObject'; -import {DragDropContext, Draggable, Droppable} from 'react-beautiful-dnd'; import type { DraggableProvided, DraggableRubric, @@ -10,18 +9,19 @@ import type { DropResult, DroppableProvided, } from 'react-beautiful-dnd'; -import AutoSizer from 'react-virtualized-auto-sizer'; +import {DragDropContext, Draggable, Droppable} from 'react-beautiful-dnd'; import type {Size} from 'react-virtualized-auto-sizer'; -import {VariableSizeList} from 'react-window'; +import AutoSizer from 'react-virtualized-auto-sizer'; import type {VariableSizeListProps} from 'react-window'; +import {VariableSizeList} from 'react-window'; -import {SelectLoadingIndicator} from '../Select/components/SelectList/SelectLoadingIndicator'; import {TextInput} from '../controls'; import {MobileContext} from '../mobile'; import {useDirection} from '../theme'; import {block} from '../utils/cn'; import {getUniqId} from '../utils/common'; +import {ListLoadingIndicator} from './ListLoadingIndicator'; import {ListItem, SimpleContainer, defaultRenderItem} from './components'; import {listNavigationIgnoredKeys} from './constants'; import type {ListItemData, ListItemProps, ListProps} from './types'; @@ -260,9 +260,7 @@ export class List extends React.Component, ListState - ); + return ; } return this.props.renderItem @@ -421,7 +419,7 @@ export class List extends React.Component, ListState void}) => { +export const ListLoadingIndicator = (props: {onIntersect?: () => void}) => { const ref = React.useRef(null); useIntersection({element: ref.current, onIntersect: props?.onIntersect}); return (
- +
); }; diff --git a/src/components/List/__tests__/List.test.tsx b/src/components/List/__tests__/List.test.tsx new file mode 100644 index 0000000000..5a1849a931 --- /dev/null +++ b/src/components/List/__tests__/List.test.tsx @@ -0,0 +1,68 @@ +import React from 'react'; + +import _ from 'react-virtualized-auto-sizer'; + +import {setupIntersectionObserverMock} from '../../../../test-utils/setupIntersectionObserverMock'; +import {cleanup, render, screen} from '../../../../test-utils/utils'; +import {List} from '../List'; +import type {ListProps} from '../types'; + +function setup(props: Partial> = {}) { + const baseProps: ListProps = { + items: ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight'], + itemsHeight: 150, + itemHeight: 28, + filterable: false, + }; + + return render( +
+ {...baseProps} {...props} /> +
, + ); +} + +const mockOnLoadMorFn = jest.fn(); + +beforeAll(() => { + setupIntersectionObserverMock(); +}); + +afterEach(() => { + cleanup(); + jest.clearAllMocks(); +}); + +describe('base List', () => { + it('should render loading indicator', () => { + setup({virtualized: false, onLoadMore: mockOnLoadMorFn, loading: true}); + + const loader = screen.getByTestId('list-loader'); + expect(loader).toBeInTheDocument(); + }); + it('should call onLoadMore callback when loading indicator is visible', () => { + setup({virtualized: false, onLoadMore: mockOnLoadMorFn, loading: true}); + + const loader = screen.getByTestId('list-loader'); + + expect(loader).toBeVisible(); + expect(mockOnLoadMorFn).toHaveBeenCalled(); + }); +}); + +describe('virtualized List', () => { + it('should render loading indicator', () => { + setup({virtualized: true, onLoadMore: mockOnLoadMorFn, loading: true}); + + const loader = screen.getByTestId('list-loader'); + expect(loader).toBeInTheDocument(); + }); + + it('should call onLoadMore callback when loading indicator is visible', () => { + setup({virtualized: true, onLoadMore: mockOnLoadMorFn, loading: true}); + + const loader = screen.getByTestId('list-loader'); + expect(loader).toBeVisible(); + expect(mockOnLoadMorFn).toHaveBeenCalled(); + }); +}); diff --git a/test-utils/setup-tests.ts b/test-utils/setup-tests.ts index f9c06f47ad..2d03f72af0 100644 --- a/test-utils/setup-tests.ts +++ b/test-utils/setup-tests.ts @@ -14,3 +14,13 @@ global.ResizeObserver = class implements ResizeObserver { observe(_target: Element, _options?: ResizeObserverOptions) {} unobserve(_target: Element) {} }; + +// mock AutoSizer to properly test functionality related to virtualization +// 400 x 400 is a random size and might be changed if needed +jest.mock( + 'react-virtualized-auto-sizer', + () => + //@ts-ignore + ({children}) => + children({height: 400, width: 400}), +); diff --git a/test-utils/setupIntersectionObserverMock.ts b/test-utils/setupIntersectionObserverMock.ts new file mode 100644 index 0000000000..7281b3d863 --- /dev/null +++ b/test-utils/setupIntersectionObserverMock.ts @@ -0,0 +1,50 @@ +export function setupIntersectionObserverMock({ + root = null, + rootMargin = '', + thresholds = [], + disconnect = () => null, + observe = () => null, + takeRecords = () => [ + { + boundingClientRect: {} as DOMRectReadOnly, + intersectionRatio: 1, + intersectionRect: {} as DOMRectReadOnly, + isIntersecting: true, + rootBounds: null, + target: {} as Element, + time: 1, + }, + ], + unobserve = () => null, +} = {}): void { + class MockIntersectionObserver implements IntersectionObserver { + readonly root: Element | null = root; + readonly rootMargin: string = rootMargin; + readonly thresholds: ReadonlyArray = thresholds; + disconnect: () => void = disconnect; + observe: (target: Element) => void = observe; + takeRecords: () => IntersectionObserverEntry[] = takeRecords; + unobserve: (target: Element) => void = unobserve; + + constructor( + callback: ( + entries: IntersectionObserverEntry[], + observer: IntersectionObserver, + ) => void, + ) { + callback(takeRecords(), this); + } + } + + Object.defineProperty(window, 'IntersectionObserver', { + writable: true, + configurable: true, + value: MockIntersectionObserver, + }); + + Object.defineProperty(global, 'IntersectionObserver', { + writable: true, + configurable: true, + value: MockIntersectionObserver, + }); +} From 5456b87620311f75dfd6ec7b657d6dacf8d09960 Mon Sep 17 00:00:00 2001 From: Gravity UI Bot <111915794+gravity-ui-bot@users.noreply.github.com> Date: Wed, 17 Apr 2024 15:34:50 +0300 Subject: [PATCH 02/24] chore(main): release 6.11.0 (#1507) --- CHANGELOG.md | 21 +++++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f97cba2b4..75383bda71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## [6.11.0](https://github.com/gravity-ui/uikit/compare/v6.10.2...v6.11.0) (2024-04-17) + + +### Features + +* **Button:** refactor to flex and center icons ([#1452](https://github.com/gravity-ui/uikit/issues/1452)) ([31c22e8](https://github.com/gravity-ui/uikit/commit/31c22e8418f67cb2df3354350f19e44471918aa4)) + + +### Bug Fixes + +* add load more functionallity to virtualized list ([#1490](https://github.com/gravity-ui/uikit/issues/1490)) ([#1513](https://github.com/gravity-ui/uikit/issues/1513)) ([8de1653](https://github.com/gravity-ui/uikit/commit/8de16536b7541835d58cc651a3ab4248f0e1ebf0)) +* **Breadcrumbs:** unset more item alignment ([#1505](https://github.com/gravity-ui/uikit/issues/1505)) ([7d9797a](https://github.com/gravity-ui/uikit/commit/7d9797ad0b1389b2e732ebeafcae312f750ae9d8)) +* **Dialog:** correct width's vars order ([#1510](https://github.com/gravity-ui/uikit/issues/1510)) ([f54da2d](https://github.com/gravity-ui/uikit/commit/f54da2de711e738a2089f26833be4dc2fa01ec6b)) +* **Label:** isolate inner z-indexes ([#1519](https://github.com/gravity-ui/uikit/issues/1519)) ([200c052](https://github.com/gravity-ui/uikit/commit/200c0522dfeedd637f0656fa27c23a2cb9f844bf)) +* **layout:** ability to override breakpoint during theme ([#1512](https://github.com/gravity-ui/uikit/issues/1512)) ([bad4fa9](https://github.com/gravity-ui/uikit/commit/bad4fa9e8336cd4b06a9875aef948fc8caaaf293)) +* **ListItemView:** fix indentation in depth more than 10 ([#1517](https://github.com/gravity-ui/uikit/issues/1517)) ([2cde017](https://github.com/gravity-ui/uikit/commit/2cde017be22a58b331a4aecff40ca3e83cb23e26)) +* **ListItemView:** some changes after feedback ([#1516](https://github.com/gravity-ui/uikit/issues/1516)) ([b274498](https://github.com/gravity-ui/uikit/commit/b2744989b4398ae7755e8ea9ae6fed0516bf8618)) +* prevent deselection of required table column items ([#1508](https://github.com/gravity-ui/uikit/issues/1508)) ([a69050c](https://github.com/gravity-ui/uikit/commit/a69050cd7213dacce7868a0259359d59e52a1743)) +* return `--g-color-private-white-20-solid` css variable ([#1511](https://github.com/gravity-ui/uikit/issues/1511)) ([4a366be](https://github.com/gravity-ui/uikit/commit/4a366be252609c1369262a2e76101e82b6d87a67)) +* **theme:** add option for controlling :root color-scheme ([#1468](https://github.com/gravity-ui/uikit/issues/1468)) ([f6237e1](https://github.com/gravity-ui/uikit/commit/f6237e1356248668693f1cc34c7515a8e6a52e41)) + ## [6.10.2](https://github.com/gravity-ui/uikit/compare/v6.10.1...v6.10.2) (2024-04-10) diff --git a/package-lock.json b/package-lock.json index c9a6086114..deb4d456e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@gravity-ui/uikit", - "version": "6.10.2", + "version": "6.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gravity-ui/uikit", - "version": "6.10.2", + "version": "6.11.0", "license": "MIT", "dependencies": { "@bem-react/classname": "^1.6.0", diff --git a/package.json b/package.json index a52ce13c8c..6e589e5901 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gravity-ui/uikit", - "version": "6.10.2", + "version": "6.11.0", "description": "Gravity UI base styling and components", "license": "MIT", "repository": { From 9ab7d7c86a06a6f94126a87acfb23033709ec8a1 Mon Sep 17 00:00:00 2001 From: "Mr.Dr.Professor Patrick" Date: Wed, 17 Apr 2024 18:00:52 +0200 Subject: [PATCH 03/24] fix: do not call warnOnce function in production (#1520) --- src/components/utils/warn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/utils/warn.ts b/src/components/utils/warn.ts index 2d1b9b6e4a..a86b0bbee7 100644 --- a/src/components/utils/warn.ts +++ b/src/components/utils/warn.ts @@ -1,7 +1,7 @@ const didWarn = new Map(); export function warnOnce(msg: string) { - if (!msg || didWarn.has(msg)) { + if (!msg || didWarn.has(msg) || process.env.NODE_ENV === 'production') { return; } From 634429aedc553419003d6ac91a97cb68a3fa313e Mon Sep 17 00:00:00 2001 From: Taya Leutina Date: Thu, 18 Apr 2024 12:26:04 +0300 Subject: [PATCH 04/24] chore(Sheet): update README (#1522) --- src/components/Sheet/README.md | 60 +++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/src/components/Sheet/README.md b/src/components/Sheet/README.md index e4bc2edab3..5f158767be 100644 --- a/src/components/Sheet/README.md +++ b/src/components/Sheet/README.md @@ -1,20 +1,50 @@ + + # Sheet -Sheet component for mobile devices - -## PropTypes - -| Name | Type | Required | Default | Description | -| :----------------------- | :--------- | :------: | :---------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| visible | `boolean` | ✓ | | Show/hide sheet | -| allowHideOnContentScroll | `boolean` | | `true` | Enable the behavior in which you can close the sheet window with a swipe down if the content is scrolled to its top (`contentNode.scrollTop === 0`) or has no scroll at all | -| hideTopBar | `boolean` | | | Hide top bar with resize handle | -| id | `string` | | `modal` | ID of the sheet, used as hash in URL. It's important to specify different `id` values if there can be more than one sheet on the page | -| title | `string` | | `undefined` | Title of the sheet window | -| className | `string` | | `undefined` | Class name for the sheet window | -| contentClassName | `string` | | `undefined` | Class name for the sheet content | -| swipeAreaClassName | `string` | | `undefined` | Class name for the swipe area | -| onClose | `function` | | `undefined` | Function called when the sheet is closed (when `visible` sets to `false`) | + + +```tsx +import {Sheet} from '@gravity-ui/uikit'; +``` + +`Sheet` is a component designed to be used in a mobile context as an information or interactive element. You can place content of any size in it - internal scrolling and dynamic resizing are supported. + +On mobile devices, you can move `Sheet` by pulling on its main part or the swipe area. To close it, swipe down or touch the area outside the `Sheet`. + +## Usage + +```tsx +import React from 'react'; +import {Button, Sheet} from '@gravity-ui/uikit'; + +const SheetExample = () => { + const [visible, setVisible] = React.useState(false); + + return ( + + + setVisible(false)} title="Content Sheet"> + Content + + + ); +}; +``` + +## Properties + +| Name | Description | Type | Default | +| :----------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------: | :---------: | +| visible | Manages `Sheet` visibility | `boolean` | `false` | +| allowHideOnContentScroll | Enable the behavior of the sheet window closing by swiping down if the content is scrolled to its top (`content Node.scrollTop === 0`) or has no scroll at all | `boolean` | `true` | +| hideTopBar | Hide top bar with resize handle | `boolean` | | +| id | ID of the sheet, used as hash in URL. It's important to specify different `id` values if there can be more than one sheet on the page | `string` | `modal` | +| title | Title of the sheet window | `string` | `undefined` | +| className | HTML `class` attribute | `string` | `undefined` | +| contentClassName | HTML `class` attribute for the sheet content | `string` | `undefined` | +| swipeAreaClassName | HTML `class` attribute for the swipe area | `string` | `undefined` | +| onClose | Handler for close event | `function` | `undefined` | ## CSS API From 7f66d28d9e3b43ebe388b90b6bc8e2799962d0c0 Mon Sep 17 00:00:00 2001 From: Isaev Alexandr Date: Thu, 18 Apr 2024 15:16:17 +0300 Subject: [PATCH 05/24] feat(useList): added ability to define initial value to useListState (#1483) Co-authored-by: Alexandr Isaev --- src/components/TreeSelect/TreeSelect.tsx | 10 +-- ...pSelectionControlledStateAndCustomIcon.tsx | 13 ++-- .../useList/__stories__/useList.mdx | 64 +++++++++++++------ src/components/useList/hooks/useListState.ts | 42 ++++++++++-- 4 files changed, 95 insertions(+), 34 deletions(-) diff --git a/src/components/TreeSelect/TreeSelect.tsx b/src/components/TreeSelect/TreeSelect.tsx index 1c271ee78b..9a9f0d4cfe 100644 --- a/src/components/TreeSelect/TreeSelect.tsx +++ b/src/components/TreeSelect/TreeSelect.tsx @@ -80,10 +80,12 @@ export const TreeSelect = React.forwardRef(function TreeSelect( }); const listState = useListState({ - expandedById, - disabledById, - activeItemId, - selectedById: selected, + controlledValues: { + expandedById, + disabledById, + activeItemId, + selectedById: selected, + }, }); const setActiveItemId = propsSetActiveItemId ?? listState.setActiveItemId; diff --git a/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx b/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx index 651cc0f0b6..b08c1c0504 100644 --- a/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx +++ b/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx @@ -5,8 +5,8 @@ import {ChevronDown, ChevronUp, Database, PlugConnection} from '@gravity-ui/icon import {Button} from '../../../Button'; import {Icon} from '../../../Icon'; import {Flex, spacing} from '../../../layout'; -import {ListItemView, getListParsedState} from '../../../useList'; -import type {ListItemCommonProps, ListItemId} from '../../../useList'; +import {ListItemView, getListParsedState, useListState} from '../../../useList'; +import type {ListItemCommonProps} from '../../../useList'; import {createRandomizedData} from '../../../useList/__stories__/utils/makeData'; import {TreeSelect} from '../../TreeSelect'; import type {TreeSelectProps} from '../../types'; @@ -40,9 +40,12 @@ export const WithGroupSelectionControlledStateAndCustomIconExample = ({ ); const [value, setValue] = React.useState([]); - const [expandedById, setExpanded] = React.useState>( - () => getListParsedState(items).initialState.expandedById, - ); + + const {expandedById, setExpanded} = useListState({ + initialValues: { + expandedById: getListParsedState(items).initialState.expandedById, + }, + }); return ( diff --git a/src/components/useList/__stories__/useList.mdx b/src/components/useList/__stories__/useList.mdx index df4c58f5ca..189da940b7 100644 --- a/src/components/useList/__stories__/useList.mdx +++ b/src/components/useList/__stories__/useList.mdx @@ -380,6 +380,32 @@ const { } = useListState(); ``` +#### props: + +```tsx +interface UseListStateProps { + /** + * Initial state values + */ + initialValues?: Partial; + /** + * Ability to pass link to another state value + */ + controlledValues?: Partial; +} +``` + +##### controlledValues example: + +```tsx +const listState = useListState(); + +// inside your component +const innerListState = useListState({ + controlledValues: listState, +}); +``` + ## Components: ### ListItemView @@ -389,33 +415,33 @@ Use it even if the functionality of the `useList` hook seems redundant to you ```tsx import { - type unstable_ListItemType as ListItemType, - unstable_ListItemView as ListItemView, + type unstable_ListItemType as ListItemType, + unstable_ListItemView as ListItemView, } from '@gravity-ui/uikit/unstable'; type Entity = {title: stirng, subtitle: string, icon: React.ReactNode}; const items: ListItemType[] = [ - {title: 'some title 1', subtitle: 'some subtitle 1', icon: }, - {title: 'some title 2', subtitle: 'some subtitle 2', icon: }, + {title: 'some title 1', subtitle: 'some subtitle 1', icon: }, + {title: 'some title 2', subtitle: 'some subtitle 2', icon: }, ]; const List = () => { - return ( - <> - {items.map(item, i) => { - return ( - - ) - }} - - ) + return ( + <> + {items.map(item, i) => { + return ( + + ) + }} + + ) }; ``` diff --git a/src/components/useList/hooks/useListState.ts b/src/components/useList/hooks/useListState.ts index 5b3165abe9..2e51d0097e 100644 --- a/src/components/useList/hooks/useListState.ts +++ b/src/components/useList/hooks/useListState.ts @@ -3,7 +3,25 @@ import React from 'react'; import type {ListState} from '../types'; -interface UseListStateProps extends Partial {} +interface UseListStateProps { + /** + * Initial state values + */ + initialValues?: Partial; + /** + * Ability to pass link to another state value + * + * ```tsx + * const listState = useListState() + * + * // inside your component + * const innerListState = useListState({ + * controlledValues: listState + * }) + * ``` + */ + controlledValues?: Partial; +} function useControlledState(value: T, defaultValue: T) { const [state, setState] = React.useState(value || defaultValue); @@ -11,11 +29,23 @@ function useControlledState(value: T, defaultValue: T) { return [value || state, setState] as const; } -export const useListState = (props: UseListStateProps = {}) => { - const [disabledById, setDisabled] = useControlledState(props.disabledById!, {}); - const [selectedById, setSelected] = useControlledState(props.selectedById!, {}); - const [expandedById, setExpanded] = useControlledState(props.expandedById!, {}); - const [activeItemId, setActiveItemId] = useControlledState(props.activeItemId, undefined); +export const useListState = ({initialValues, controlledValues}: UseListStateProps = {}) => { + const [disabledById, setDisabled] = useControlledState( + controlledValues?.disabledById!, + initialValues?.disabledById || {}, + ); + const [selectedById, setSelected] = useControlledState( + controlledValues?.selectedById!, + initialValues?.selectedById || {}, + ); + const [expandedById, setExpanded] = useControlledState( + controlledValues?.expandedById!, + initialValues?.expandedById || {}, + ); + const [activeItemId, setActiveItemId] = useControlledState( + controlledValues?.activeItemId, + initialValues?.activeItemId, + ); return { disabledById, From 648b28b809fcd8a74d813bb96f4ddfd3f6359d36 Mon Sep 17 00:00:00 2001 From: Gravity UI Bot <111915794+gravity-ui-bot@users.noreply.github.com> Date: Thu, 18 Apr 2024 16:32:07 +0300 Subject: [PATCH 06/24] chore(main): release 6.12.0 (#1521) --- CHANGELOG.md | 12 ++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75383bda71..2488a80b4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [6.12.0](https://github.com/gravity-ui/uikit/compare/v6.11.0...v6.12.0) (2024-04-18) + + +### Features + +* **useList:** added ability to define initial value to useListState ([#1483](https://github.com/gravity-ui/uikit/issues/1483)) ([7f66d28](https://github.com/gravity-ui/uikit/commit/7f66d28d9e3b43ebe388b90b6bc8e2799962d0c0)) + + +### Bug Fixes + +* do not call warnOnce function in production ([#1520](https://github.com/gravity-ui/uikit/issues/1520)) ([9ab7d7c](https://github.com/gravity-ui/uikit/commit/9ab7d7c86a06a6f94126a87acfb23033709ec8a1)) + ## [6.11.0](https://github.com/gravity-ui/uikit/compare/v6.10.2...v6.11.0) (2024-04-17) diff --git a/package-lock.json b/package-lock.json index deb4d456e0..eafce34a02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@gravity-ui/uikit", - "version": "6.11.0", + "version": "6.12.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gravity-ui/uikit", - "version": "6.11.0", + "version": "6.12.0", "license": "MIT", "dependencies": { "@bem-react/classname": "^1.6.0", diff --git a/package.json b/package.json index 6e589e5901..e925d99f98 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gravity-ui/uikit", - "version": "6.11.0", + "version": "6.12.0", "description": "Gravity UI base styling and components", "license": "MIT", "repository": { From d89610a8c7bc952ecf0db4c2548c8c5dde9bc710 Mon Sep 17 00:00:00 2001 From: Isaev Alexandr Date: Fri, 19 Apr 2024 16:22:43 +0300 Subject: [PATCH 07/24] feat(layout): unification of Theme and Layout Providers (#1518) Co-authored-by: Alexandr Isaev --- .../layout/LayoutProvider/LayoutProvider.tsx | 22 ++++--- .../LayoutProvider/__stories__/Layout.mdx | 63 ++++++------------- .../demo/LayoutPresenter/LayoutPresenter.tsx | 6 +- src/components/layout/index.ts | 2 +- src/components/theme/ThemeProvider.tsx | 54 +++++++++------- 5 files changed, 66 insertions(+), 81 deletions(-) diff --git a/src/components/layout/LayoutProvider/LayoutProvider.tsx b/src/components/layout/LayoutProvider/LayoutProvider.tsx index 2152a7dbe1..2df3977a0d 100644 --- a/src/components/layout/LayoutProvider/LayoutProvider.tsx +++ b/src/components/layout/LayoutProvider/LayoutProvider.tsx @@ -6,21 +6,16 @@ import {useCurrentActiveMediaQuery} from '../hooks/useCurrentActiveMediaQuery'; import type {LayoutTheme, MediaType, RecursivePartial} from '../types'; import {makeLayoutDefaultTheme} from '../utils/makeLayoutDefaultTheme'; -interface LayoutProviderProps { +export interface LayoutProviderProps { theme?: RecursivePartial; /** - * During ssr you can override default (`s`) media screen size + * During ssr you can override default (`s`) media screen size if needed */ initialMediaQuery?: MediaType; children: React.ReactNode; } -/** - * Provide context for layout components and current media queries. - * --- - * Storybook - https://preview.gravity-ui.com/uikit/?path=/docs/layout--playground#layoutprovider-and-layouttheme - */ -export function LayoutProvider({ +export function PrivateLayoutProvider({ children, theme: override, initialMediaQuery, @@ -39,3 +34,14 @@ export function LayoutProvider({ ); } + +/** + * @deprecated - already used as part of ThemeProvider. To override layout theme use `layout` prop + * + * Provide context for layout components and current media queries. + * --- + * Storybook - https://preview.gravity-ui.com/uikit/?path=/docs/layout--playground#layoutprovider-and-layouttheme + */ +export function LayoutProvider({children}: LayoutProviderProps) { + return children; +} diff --git a/src/components/layout/LayoutProvider/__stories__/Layout.mdx b/src/components/layout/LayoutProvider/__stories__/Layout.mdx index 1e9ff003ca..3306d2eaf4 100644 --- a/src/components/layout/LayoutProvider/__stories__/Layout.mdx +++ b/src/components/layout/LayoutProvider/__stories__/Layout.mdx @@ -32,7 +32,6 @@ import {Container, Row, Col, Flex} from '@gravity-ui/uikit'; ### Components: -- [LayoutProvider and LayoutTheme](#layoutprovider-and-layouttheme) - [Layout Grid](#layout-grid) - [Row](#row) - [Col](#col) @@ -75,17 +74,17 @@ _You can override default values on project level:_ ``` ```tsx -import {LayoutProvider, LayoutTheme} from '@gravity-ui/uikit'; +import {ThemeProvider, LayoutTheme} from '@gravity-ui/uikit'; -const layoutTheme: LayoutTheme = { +const theme: LayoutTheme = { spaceBaseSize: 5, }; export const App = () => { return ( - + {...} - + ); }; ``` @@ -121,55 +120,29 @@ We use `mobile-first` approach. It means that you should adapt you app to deskto > To override breakpoint use `theme` breakpoints property; ```tsx - export const APP_LAYOUT_THEME: LayoutTheme = { + const APP_LAYOUT_THEME: LayoutTheme = { + spaceBaseSize: 4, + components: { + container: { + gutters: 3, + media: { + l: { + gutters: 5, + }, + }, + }, + }, breakpoints: { s: 320, l: 980, } }; - + {...} - + ``` -## LayoutProvider and LayoutTheme - -Through `LayoutProvider` components can get default props which are corresponding to different screen sizes. - -Usage of `LayoutProvider` is optional. Use it if you need to override default spacing or add default behaviour to connected to theme compoennts (now only `Container`) - -### props: - -- `theme` - partial LayoutTheme onject that will be override original theme; -- `initialMediaQuery` - use can directly pass initial - -```tsx -import {LayoutProvider, LayoutTheme} from '@gravity-ui/uikit'; - -export const APP_LAYOUT_THEME: LayoutTheme = { - spaceBaseSize: 4, - components: { - container: { - gutters: 3, - media: { - l: { - gutters: 5, - }, - }, - }, - }, -}; - -export const App = () => { - return ( - - - - ); -}; -``` - ## Box The `Box` component is a developer friend and basic block to build other components. Aware about spacing, its own sizes and most commonly used CSS properties. diff --git a/src/components/layout/demo/LayoutPresenter/LayoutPresenter.tsx b/src/components/layout/demo/LayoutPresenter/LayoutPresenter.tsx index af3b47e93b..48f9c70944 100644 --- a/src/components/layout/demo/LayoutPresenter/LayoutPresenter.tsx +++ b/src/components/layout/demo/LayoutPresenter/LayoutPresenter.tsx @@ -2,8 +2,8 @@ import React from 'react'; import {Text} from '../../../Text'; import type {LayoutTheme} from '../../../layout'; +import {ThemeProvider} from '../../../theme'; import {Flex} from '../../Flex/Flex'; -import {LayoutProvider} from '../../LayoutProvider/LayoutProvider'; import {useLayoutContext} from '../../hooks/useLayoutContext'; import {sp} from '../../spacing/spacing'; @@ -35,7 +35,7 @@ function Title({title}: {title?: string}) { export const LayoutPresenter = ({children, title, theme}: LayoutPresenterProps) => { return ( - + <div style={{ @@ -46,6 +46,6 @@ export const LayoutPresenter = ({children, title, theme}: LayoutPresenterProps) > {children} </div> - </LayoutProvider> + </ThemeProvider> ); }; diff --git a/src/components/layout/index.ts b/src/components/layout/index.ts index 3a565b5525..51a6a55bd6 100644 --- a/src/components/layout/index.ts +++ b/src/components/layout/index.ts @@ -4,7 +4,7 @@ export * from './Row/Row'; export * from './Flex/Flex'; export * from './Box/Box'; export * from './Container/Container'; -export * from './LayoutProvider/LayoutProvider'; +export {LayoutProvider} from './LayoutProvider/LayoutProvider'; export * from './spacing/spacing'; export * from './hooks/useLayoutContext'; diff --git a/src/components/theme/ThemeProvider.tsx b/src/components/theme/ThemeProvider.tsx index 5fc22bc764..53ba935a54 100644 --- a/src/components/theme/ThemeProvider.tsx +++ b/src/components/theme/ThemeProvider.tsx @@ -1,5 +1,7 @@ import React from 'react'; +import {PrivateLayoutProvider} from '../layout/LayoutProvider/LayoutProvider'; +import type {LayoutProviderProps} from '../layout/LayoutProvider/LayoutProvider'; import {block} from '../utils/cn'; import {ThemeContext} from './ThemeContext'; @@ -26,6 +28,7 @@ export interface ThemeProviderProps extends React.PropsWithChildren<{}> { nativeScrollbar?: boolean; scoped?: boolean; rootClassName?: string; + layout?: Omit<LayoutProviderProps, 'children'>; } export function ThemeProvider({ @@ -37,6 +40,7 @@ export function ThemeProvider({ scoped: scopedProp = false, rootClassName = '', children, + layout, }: ThemeProviderProps) { const parentThemeState = React.useContext(ThemeContext); const systemThemeState = React.useContext(ThemeSettingsContext); @@ -86,30 +90,32 @@ export function ThemeProvider({ ); return ( - <ThemeContext.Provider value={contextValue}> - <ThemeSettingsContext.Provider value={themeSettingsContext}> - {scoped ? ( - <div - className={b( - { - theme: themeValue, - 'native-scrollbar': nativeScrollbar !== false, - }, - rootClassName, - )} - dir={ - hasParentProvider && direction === parentDirection - ? undefined - : direction - } - > - {children} - </div> - ) : ( - children - )} - </ThemeSettingsContext.Provider> - </ThemeContext.Provider> + <PrivateLayoutProvider {...layout}> + <ThemeContext.Provider value={contextValue}> + <ThemeSettingsContext.Provider value={themeSettingsContext}> + {scoped ? ( + <div + className={b( + { + theme: themeValue, + 'native-scrollbar': nativeScrollbar !== false, + }, + rootClassName, + )} + dir={ + hasParentProvider && direction === parentDirection + ? undefined + : direction + } + > + {children} + </div> + ) : ( + children + )} + </ThemeSettingsContext.Provider> + </ThemeContext.Provider> + </PrivateLayoutProvider> ); } From ba779214029c22039eae8b9d2fff4d46f78fbf82 Mon Sep 17 00:00:00 2001 From: "Mr.Dr.Professor Patrick" <alaev89@yandex-team.ru> Date: Fri, 19 Apr 2024 18:09:37 +0200 Subject: [PATCH 08/24] chore(TextArea): add resizable example to readme (#1527) --- src/components/controls/TextArea/README.md | 32 +++++++++++++++++++ .../controls/TextInput/TextInput.tsx | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/components/controls/TextArea/README.md b/src/components/controls/TextArea/README.md index 40324ee213..7de9b89a05 100644 --- a/src/components/controls/TextArea/README.md +++ b/src/components/controls/TextArea/README.md @@ -180,6 +180,38 @@ The row count of the `TextArea` is controlled by the `rows`, `minRows` and `maxR </ExampleBlock> LANDING_BLOCK--> +## Resizable TextArea + +You can get resizable behaviour by providing `resize` style to `controlProps` property. + +<!--LANDING_BLOCK +<ExampleBlock + code={` +<TextArea + rows={4} + placeholder="Placeholder" + style={{width: "auto", maxWidth: "100%"}} + controlProps={{style: {resize: "both"}}} +/> +`} +> + <UIKit.TextArea + rows={4} + placeholder="Placeholder" + style={{width: "auto", maxWidth: "100%"}} + controlProps={{style: {resize: "both"}}} + /> +</ExampleBlock> +LANDING_BLOCK--> + +<!--GITHUB_BLOCK--> + +```tsx +<TextArea rows={4} controlProps={{style: {resize: 'both'}}} /> +``` + +<!--/GITHUB_BLOCK--> + ## Properties | Name | Description | Type | Default | diff --git a/src/components/controls/TextInput/TextInput.tsx b/src/components/controls/TextInput/TextInput.tsx index 343bfed167..6c4e189525 100644 --- a/src/components/controls/TextInput/TextInput.tsx +++ b/src/components/controls/TextInput/TextInput.tsx @@ -55,8 +55,8 @@ export type TextInputPin = InputControlPin; export type TextInputSize = InputControlSize; export type TextInputView = InputControlView; -// eslint-disable-next-line complexity export const TextInput = React.forwardRef<HTMLSpanElement, TextInputProps>( + // eslint-disable-next-line complexity function TextInput(props, ref) { const { view = 'normal', From 62431ca72c220f9a434d065c87b33cf060cd7f7a Mon Sep 17 00:00:00 2001 From: Isaev Alexandr <IsaevAlexandr188@gmail.com> Date: Fri, 19 Apr 2024 20:19:55 +0300 Subject: [PATCH 09/24] fix(ThemeProvider): changed theme to config prop name (#1528) Co-authored-by: Alexandr Isaev <aisaev188@yandex-team.ru> --- .../layout/LayoutProvider/LayoutProvider.tsx | 17 +++++++++++++---- .../demo/LayoutPresenter/LayoutPresenter.tsx | 4 ++-- src/components/theme/ThemeProvider.tsx | 4 ++-- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/components/layout/LayoutProvider/LayoutProvider.tsx b/src/components/layout/LayoutProvider/LayoutProvider.tsx index 2df3977a0d..37cc58fa07 100644 --- a/src/components/layout/LayoutProvider/LayoutProvider.tsx +++ b/src/components/layout/LayoutProvider/LayoutProvider.tsx @@ -6,8 +6,8 @@ import {useCurrentActiveMediaQuery} from '../hooks/useCurrentActiveMediaQuery'; import type {LayoutTheme, MediaType, RecursivePartial} from '../types'; import {makeLayoutDefaultTheme} from '../utils/makeLayoutDefaultTheme'; -export interface LayoutProviderProps { - theme?: RecursivePartial<LayoutTheme>; +export interface PrivateLayoutProviderProps { + config?: RecursivePartial<LayoutTheme>; /** * During ssr you can override default (`s`) media screen size if needed */ @@ -17,9 +17,9 @@ export interface LayoutProviderProps { export function PrivateLayoutProvider({ children, - theme: override, + config: override, initialMediaQuery, -}: LayoutProviderProps) { +}: PrivateLayoutProviderProps) { const theme = React.useMemo(() => makeLayoutDefaultTheme({override}), [override]); const activeMediaQuery = useCurrentActiveMediaQuery(theme.breakpoints, initialMediaQuery); @@ -35,6 +35,15 @@ export function PrivateLayoutProvider({ ); } +interface LayoutProviderProps { + theme?: RecursivePartial<LayoutTheme>; + /** + * During ssr you can override default (`s`) media screen size if needed + */ + initialMediaQuery?: MediaType; + children: React.ReactNode; +} + /** * @deprecated - already used as part of ThemeProvider. To override layout theme use `layout` prop * diff --git a/src/components/layout/demo/LayoutPresenter/LayoutPresenter.tsx b/src/components/layout/demo/LayoutPresenter/LayoutPresenter.tsx index 48f9c70944..d53ff07ee2 100644 --- a/src/components/layout/demo/LayoutPresenter/LayoutPresenter.tsx +++ b/src/components/layout/demo/LayoutPresenter/LayoutPresenter.tsx @@ -33,9 +33,9 @@ function Title({title}: {title?: string}) { ); } -export const LayoutPresenter = ({children, title, theme}: LayoutPresenterProps) => { +export const LayoutPresenter = ({children, title, theme: config}: LayoutPresenterProps) => { return ( - <ThemeProvider layout={{theme}}> + <ThemeProvider layout={{config}}> <Title title={title} /> <div style={{ diff --git a/src/components/theme/ThemeProvider.tsx b/src/components/theme/ThemeProvider.tsx index 53ba935a54..c982e03c1c 100644 --- a/src/components/theme/ThemeProvider.tsx +++ b/src/components/theme/ThemeProvider.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {PrivateLayoutProvider} from '../layout/LayoutProvider/LayoutProvider'; -import type {LayoutProviderProps} from '../layout/LayoutProvider/LayoutProvider'; +import type {PrivateLayoutProviderProps} from '../layout/LayoutProvider/LayoutProvider'; import {block} from '../utils/cn'; import {ThemeContext} from './ThemeContext'; @@ -28,7 +28,7 @@ export interface ThemeProviderProps extends React.PropsWithChildren<{}> { nativeScrollbar?: boolean; scoped?: boolean; rootClassName?: string; - layout?: Omit<LayoutProviderProps, 'children'>; + layout?: Omit<PrivateLayoutProviderProps, 'children'>; } export function ThemeProvider({ From d7d45db53338b5e623bb81b59e7b66a588c18e28 Mon Sep 17 00:00:00 2001 From: LakeVostok <lakevostok@yandex.ru> Date: Mon, 22 Apr 2024 11:32:56 +0300 Subject: [PATCH 10/24] fix(ListItemView): removed max identation (#1535) --- .../useList/components/ListItemView/ListItemView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/useList/components/ListItemView/ListItemView.tsx b/src/components/useList/components/ListItemView/ListItemView.tsx index 79cfed8203..aff283be05 100644 --- a/src/components/useList/components/ListItemView/ListItemView.tsx +++ b/src/components/useList/components/ListItemView/ListItemView.tsx @@ -60,7 +60,7 @@ export interface ListItemViewProps extends QAProps, ListItemCommonProps { } interface SlotProps extends FlexProps { - indentation?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; + indentation?: number; } export const ListItemViewSlot = ({ @@ -77,7 +77,7 @@ export const ListItemViewSlot = ({ }; const renderSafeIndentation = (indentation?: number) => { - if (indentation && indentation >= 1 && indentation < 11) { + if (indentation && indentation >= 1) { return ( <ListItemViewSlot indentation={Math.floor(indentation) as SlotProps['indentation']} /> ); From 2ae45ef9a997f8d0cdd8687116044239e1ba710c Mon Sep 17 00:00:00 2001 From: Isaev Alexandr <aisaev188@yandex-team.ru> Date: Mon, 22 Apr 2024 15:52:46 +0300 Subject: [PATCH 11/24] fix(ListItemView): fix bg color (#1488) --- .../useList/components/ListItemView/ListItemView.scss | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/useList/components/ListItemView/ListItemView.scss b/src/components/useList/components/ListItemView/ListItemView.scss index 060bd41af5..5fab17fa17 100644 --- a/src/components/useList/components/ListItemView/ListItemView.scss +++ b/src/components/useList/components/ListItemView/ListItemView.scss @@ -12,9 +12,8 @@ $block: '.#{variables.$ns}list-item-view'; } &:hover#{$block}_activeOnHover, - &_active#{$block}_activeOnHover, &_active { - background: var(--g-color-base-simple-hover-solid); + background: var(--g-color-base-simple-hover); } &_clickable { @@ -22,11 +21,17 @@ $block: '.#{variables.$ns}list-item-view'; } &_selected, - &_selected:not(#{$block}_dragging)#{$block}_active, // if active and selected selected bgc more priority + &_selected#{$block}_active, // if active and selected selected bgc more priority &_selected:hover#{$block}_activeOnHover { background: var(--g-color-base-selection); } + &_dragging, + &_dragging#{$block}_selected, + &_dragging#{$block}_active { + background: var(--g-color-base-simple-hover-solid); + } + &_radius_s { border-radius: var(--g-list-item-border-radius, 3px); } From 39b7fe801593151094759503e4f8c706cece38c8 Mon Sep 17 00:00:00 2001 From: Andrey Morozov <amje@yandex-team.ru> Date: Mon, 22 Apr 2024 16:14:05 +0300 Subject: [PATCH 12/24] fix(Card): reset box shadow styles for the root (#1537) --- src/components/Card/Card.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Card/Card.scss b/src/components/Card/Card.scss index 16169f73ad..eb5ea5ff3d 100644 --- a/src/components/Card/Card.scss +++ b/src/components/Card/Card.scss @@ -6,6 +6,7 @@ $block: '.#{variables.$ns}card'; --_--background-color: transparent; --_--border-color: transparent; --_--border-width: 0; + --_--box-shadow: none; box-shadow: var(--g-card-box-shadow, var(--_--box-shadow)); box-sizing: border-box; From 90f24bd2080a19ef6c3870af7d631b82c7724481 Mon Sep 17 00:00:00 2001 From: Gravity UI Bot <111915794+gravity-ui-bot@users.noreply.github.com> Date: Mon, 22 Apr 2024 17:22:12 +0300 Subject: [PATCH 13/24] chore(main): release 6.13.0 (#1525) --- CHANGELOG.md | 15 +++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2488a80b4d..019a46b540 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [6.13.0](https://github.com/gravity-ui/uikit/compare/v6.12.0...v6.13.0) (2024-04-22) + + +### Features + +* **layout:** unification of Theme and Layout Providers ([#1518](https://github.com/gravity-ui/uikit/issues/1518)) ([d89610a](https://github.com/gravity-ui/uikit/commit/d89610a8c7bc952ecf0db4c2548c8c5dde9bc710)) + + +### Bug Fixes + +* **Card:** reset box shadow styles for the root ([#1537](https://github.com/gravity-ui/uikit/issues/1537)) ([39b7fe8](https://github.com/gravity-ui/uikit/commit/39b7fe801593151094759503e4f8c706cece38c8)) +* **ListItemView:** fix bg color ([#1488](https://github.com/gravity-ui/uikit/issues/1488)) ([2ae45ef](https://github.com/gravity-ui/uikit/commit/2ae45ef9a997f8d0cdd8687116044239e1ba710c)) +* **ListItemView:** removed max identation ([#1535](https://github.com/gravity-ui/uikit/issues/1535)) ([d7d45db](https://github.com/gravity-ui/uikit/commit/d7d45db53338b5e623bb81b59e7b66a588c18e28)) +* **ThemeProvider:** changed theme to config prop name ([#1528](https://github.com/gravity-ui/uikit/issues/1528)) ([62431ca](https://github.com/gravity-ui/uikit/commit/62431ca72c220f9a434d065c87b33cf060cd7f7a)) + ## [6.12.0](https://github.com/gravity-ui/uikit/compare/v6.11.0...v6.12.0) (2024-04-18) diff --git a/package-lock.json b/package-lock.json index eafce34a02..3824a4a886 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@gravity-ui/uikit", - "version": "6.12.0", + "version": "6.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gravity-ui/uikit", - "version": "6.12.0", + "version": "6.13.0", "license": "MIT", "dependencies": { "@bem-react/classname": "^1.6.0", diff --git a/package.json b/package.json index e925d99f98..28f5d772e9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@gravity-ui/uikit", - "version": "6.12.0", + "version": "6.13.0", "description": "Gravity UI base styling and components", "license": "MIT", "repository": { From f91668105abdb06a59891872b3202f5d0152bb9e Mon Sep 17 00:00:00 2001 From: Isaev Alexandr <aisaev188@yandex-team.ru> Date: Tue, 23 Apr 2024 15:06:58 +0300 Subject: [PATCH 14/24] fix(Alert): fix close button width (#1542) --- src/components/Alert/Alert.scss | 4 ++++ src/components/Alert/Alert.tsx | 1 + src/components/Alert/__snapshots__/Alert.test.tsx.snap | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/Alert/Alert.scss b/src/components/Alert/Alert.scss index de329d75f3..34b4ede0ca 100644 --- a/src/components/Alert/Alert.scss +++ b/src/components/Alert/Alert.scss @@ -14,4 +14,8 @@ $block: '.#{variables.$ns}alert'; &__actions_minContent { width: min-content; } + + &__close-btn { + flex-shrink: 0; + } } diff --git a/src/components/Alert/Alert.tsx b/src/components/Alert/Alert.tsx index 902340d6ef..97e75f23ae 100644 --- a/src/components/Alert/Alert.tsx +++ b/src/components/Alert/Alert.tsx @@ -66,6 +66,7 @@ export const Alert = (props: AlertProps) => { {onClose && ( <Button view="flat" + className={bAlert('close-btn')} onClick={onClose} extraProps={{ 'aria-label': i18n('label_close'), diff --git a/src/components/Alert/__snapshots__/Alert.test.tsx.snap b/src/components/Alert/__snapshots__/Alert.test.tsx.snap index 300477728f..d17001bf97 100644 --- a/src/components/Alert/__snapshots__/Alert.test.tsx.snap +++ b/src/components/Alert/__snapshots__/Alert.test.tsx.snap @@ -73,7 +73,7 @@ exports[`Alert has predicted styles if inline layout rendered 1`] = ` </div> <button aria-label="Close" - class="g-button g-button_view_flat g-button_size_m g-button_pin_round-round" + class="g-button g-button_view_flat g-button_size_m g-button_pin_round-round g-alert__close-btn" type="button" > <span From 6dc27207ff733f07fafea54538ed492bfa2f4593 Mon Sep 17 00:00:00 2001 From: Kyzyl-ool Kezhik <kyzyloolk@mail.ru> Date: Tue, 23 Apr 2024 14:42:26 +0200 Subject: [PATCH 15/24] ci(storybook): fix a11y issues (#1538) Co-authored-by: Kezhik Kyzyl-ool <k.kyzylool@nebius.com> --- .storybook/test-runner.ts | 2 ++ .../Button/__stories__/Button.stories.tsx | 2 +- .../Divider/__stories__/Divider.stories.tsx | 17 +++++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/.storybook/test-runner.ts b/.storybook/test-runner.ts index 2d42a0b921..f4eae1c4cb 100644 --- a/.storybook/test-runner.ts +++ b/.storybook/test-runner.ts @@ -17,9 +17,11 @@ const config: TestRunnerConfig = { // Apply story-level a11y rules await configureAxe(page, { rules: storyContext.parameters?.a11y?.config?.rules, + reporter: 'no-passes', }); await checkA11y(page, '#storybook-root', { + verbose: false, detailedReport: true, detailedReportOptions: { html: true, diff --git a/src/components/Button/__stories__/Button.stories.tsx b/src/components/Button/__stories__/Button.stories.tsx index 13e4888f5a..68a5363f5a 100644 --- a/src/components/Button/__stories__/Button.stories.tsx +++ b/src/components/Button/__stories__/Button.stories.tsx @@ -180,7 +180,7 @@ export const InsideText: Story = { <Button {...args} /> dolor <br /> sit{' '} - <Button {...args}> + <Button {...args} extraProps={{'aria-label': 'Icon button inside text'}}> <IconComponent data={Globe} /> </Button>{' '} amet diff --git a/src/components/Divider/__stories__/Divider.stories.tsx b/src/components/Divider/__stories__/Divider.stories.tsx index ed948837e7..16e89a446b 100644 --- a/src/components/Divider/__stories__/Divider.stories.tsx +++ b/src/components/Divider/__stories__/Divider.stories.tsx @@ -11,6 +11,23 @@ import {Divider} from '../Divider'; const meta: Meta<typeof Divider> = { title: 'Components/Utils/Divider', component: Divider, + parameters: { + a11y: { + element: '#storybook-root', + config: { + rules: [ + { + id: 'aria-allowed-attr', // https://github.com/gravity-ui/uikit/issues/1336 + enabled: false, + }, + { + id: 'aria-required-parent', // https://github.com/gravity-ui/uikit/issues/1336 + enabled: false, + }, + ], + }, + }, + }, }; export default meta; From 41cc3a5512a0f730e743e4efed6e9d0767c68a6e Mon Sep 17 00:00:00 2001 From: Kyzyl-ool Kezhik <kyzyloolk@mail.ru> Date: Tue, 23 Apr 2024 15:45:59 +0200 Subject: [PATCH 16/24] ci(storybook): make tests mandatory (#1543) Co-authored-by: Kezhik Kyzyl-ool <k.kyzylool@nebius.com> --- .github/workflows/pr-storybook-tests.yml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pr-storybook-tests.yml b/.github/workflows/pr-storybook-tests.yml index 7198f7d67a..2b62c692e1 100644 --- a/.github/workflows/pr-storybook-tests.yml +++ b/.github/workflows/pr-storybook-tests.yml @@ -1,10 +1,7 @@ name: PR Storybook Tests on: - workflow_run: - workflows: ['PR Preview Deploy'] - types: - - completed + pull_request: jobs: tests: @@ -19,7 +16,12 @@ jobs: run: npm ci - name: Install Playwright Browsers run: npx playwright install --with-deps - - name: Storybook Tests + - name: Build Storybook Static + run: npm run build-storybook + - name: Serve Storybook and run tests + run: | + npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \ + "npx http-server storybook-static --port 7007 --silent" \ + "npx wait-on $PR_PREVIEW_URL && npm run test-storybook" env: - PR_PREVIEW_URL: "https://preview.gravity-ui.com/uikit/${{github.event.pull_request.number}}" - run: npm run test-storybook + PR_PREVIEW_URL: http://127.0.0.1:7007 From 0ab21bbc204f5e26dccfaea865331d3f984af8ed Mon Sep 17 00:00:00 2001 From: Kirill Dyachkovskiy <81510334+KirillDyachkovskiy@users.noreply.github.com> Date: Tue, 23 Apr 2024 17:03:19 +0300 Subject: [PATCH 17/24] feat(Table): close table actions popup after click (#1546) --- .../Table/hoc/withTableActions/withTableActions.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/Table/hoc/withTableActions/withTableActions.tsx b/src/components/Table/hoc/withTableActions/withTableActions.tsx index 8a988ce363..cdb2f3145b 100644 --- a/src/components/Table/hoc/withTableActions/withTableActions.tsx +++ b/src/components/Table/hoc/withTableActions/withTableActions.tsx @@ -4,6 +4,7 @@ import {Ellipsis} from '@gravity-ui/icons'; import _memoize from 'lodash/memoize'; import {useUniqId} from '../../../../hooks'; +import {useBoolean} from '../../../../hooks/private'; import type {PopperPlacement} from '../../../../hooks/private'; import {Button} from '../../../Button'; import {Icon} from '../../../Icon'; @@ -112,7 +113,7 @@ const DefaultRowActions = <I extends TableDataItem>({ rowActionsSize, isRowDisabled, }: DefaultRowActionsProps<I>) => { - const [open, setOpen] = React.useState(false); + const [isPopupOpen, , closePopup, togglePopup] = useBoolean(false); const anchorRef = React.useRef<HTMLButtonElement>(null); const rowId = useUniqId(); @@ -137,6 +138,8 @@ const DefaultRowActions = <I extends TableDataItem>({ onClick={(event) => { event.stopPropagation(); handler(item, index, event); + + closePopup(); }} iconStart={icon} className={menuItemCn} @@ -158,10 +161,10 @@ const DefaultRowActions = <I extends TableDataItem>({ return ( <div className={actionsCn}> <Popup - open={open} + open={isPopupOpen} anchorRef={anchorRef} placement={DEFAULT_PLACEMENT} - onOutsideClick={() => setOpen(false)} + onOutsideClick={closePopup} id={rowId} > <Menu className={menuCn} size={rowActionsSize}> @@ -171,13 +174,13 @@ const DefaultRowActions = <I extends TableDataItem>({ <Button view="flat-secondary" className={actionsButtonCn} - onClick={() => setOpen(!open)} + onClick={togglePopup} size={rowActionsSize} ref={anchorRef} disabled={disabled} extraProps={{ 'aria-label': i18n('label-actions'), - 'aria-expanded': open, + 'aria-expanded': isPopupOpen, 'aria-controls': rowId, }} > From c34590e40fa49333f990271ec70dbbd290c51b86 Mon Sep 17 00:00:00 2001 From: imechoim <echernovol@yandex-team.ru> Date: Tue, 23 Apr 2024 17:29:49 +0200 Subject: [PATCH 18/24] fix(Select): add onLoadMore to useCallback deps in SelectList (#1041) From 57031843c1c9b8905793927b69f9ed16b41220e1 Mon Sep 17 00:00:00 2001 From: Isaev Alexandr <aisaev188@yandex-team.ru> Date: Wed, 24 Apr 2024 16:24:17 +0300 Subject: [PATCH 19/24] chore(layout): update codeowners file (#1551) --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index 51114770a0..2dd3d5b98a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -50,6 +50,7 @@ /src/components/controls/TextInput @korvin89 /src/components/Toaster @ogonkov /src/components/Tooltip @amje +/src/components/layout @IsaevAlexandr /src/hooks/useActionHandlers @ogonkov /src/hooks/useFileInput @korvin89 From 8c7ca2dca5a4a52cf9714b25aa8c8d85d4394f39 Mon Sep 17 00:00:00 2001 From: Valerii Sidorenko <balepas@nebius.com> Date: Wed, 24 Apr 2024 16:43:48 +0200 Subject: [PATCH 20/24] feat(Portal): take in consideration parent theme (#1506) --- src/components/Modal/Modal.tsx | 58 ++++++------ src/components/Popup/Popup.tsx | 50 +++++----- src/components/Portal/Portal.scss | 9 ++ src/components/Portal/Portal.tsx | 21 ++++- .../layout/LayoutProvider/LayoutProvider.tsx | 20 ++-- .../hooks/useCurrentActiveMediaQuery.tsx | 23 +---- .../layout/utils/makeLayoutDefaultTheme.ts | 18 ---- .../layout/utils/overrideLayoutTheme.ts | 16 ++++ src/components/theme/ThemeProvider.tsx | 9 +- .../theme/__stories__/Theme.stories.tsx | 92 +++++++++++++------ src/components/theme/getDarkMediaMatch.ts | 2 + src/components/theme/getSystemTheme.ts | 4 +- src/components/theme/types.ts | 1 + src/components/theme/useSystemTheme.ts | 6 +- 14 files changed, 189 insertions(+), 140 deletions(-) create mode 100644 src/components/Portal/Portal.scss delete mode 100644 src/components/layout/utils/makeLayoutDefaultTheme.ts create mode 100644 src/components/layout/utils/overrideLayoutTheme.ts diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 2c220ee022..7d14950d1b 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -102,34 +102,32 @@ export function Modal({ }); return ( - <Portal container={container}> - <CSSTransition - nodeRef={containerRef} - in={open} - addEndListener={(done) => - containerRef.current?.addEventListener('animationend', done) - } - classNames={getCSSTransitionClassNames(b)} - mountOnEnter={!keepMounted} - unmountOnExit={!keepMounted} - appear={true} - onEnter={() => { - setInTransition(true); - onTransitionEnter?.(); - }} - onExit={() => { - setInTransition(true); - onTransitionExit?.(); - }} - onEntered={() => { - setInTransition(false); - onTransitionEntered?.(); - }} - onExited={() => { - setInTransition(false); - onTransitionExited?.(); - }} - > + <CSSTransition + nodeRef={containerRef} + in={open} + addEndListener={(done) => containerRef.current?.addEventListener('animationend', done)} + classNames={getCSSTransitionClassNames(b)} + mountOnEnter={!keepMounted} + unmountOnExit={!keepMounted} + appear={true} + onEnter={() => { + setInTransition(true); + onTransitionEnter?.(); + }} + onExit={() => { + setInTransition(true); + onTransitionExit?.(); + }} + onEntered={() => { + setInTransition(false); + onTransitionEntered?.(); + }} + onExited={() => { + setInTransition(false); + onTransitionExited?.(); + }} + > + <Portal container={container}> <div ref={containerRef} style={style} className={b({open}, className)} data-qa={qa}> <div className={b('content-aligner')}> <div className={b('content-wrapper')}> @@ -157,7 +155,7 @@ export function Modal({ </div> </div> </div> - </CSSTransition> - </Portal> + </Portal> + </CSSTransition> ); } diff --git a/src/components/Popup/Popup.tsx b/src/components/Popup/Popup.tsx index f0e436b897..b25b2e03cb 100644 --- a/src/components/Popup/Popup.tsx +++ b/src/components/Popup/Popup.tsx @@ -151,30 +151,28 @@ export function Popup({ }); return ( - <Portal container={container} disablePortal={disablePortal}> - <CSSTransition - nodeRef={containerRef} - in={open} - addEndListener={(done) => - containerRef.current?.addEventListener('animationend', done) - } - classNames={getCSSTransitionClassNames(b)} - mountOnEnter={!keepMounted} - unmountOnExit={!keepMounted} - appear={true} - onEnter={() => { - onTransitionEnter?.(); - }} - onEntered={() => { - onTransitionEntered?.(); - }} - onExit={() => { - onTransitionExit?.(); - }} - onExited={() => { - onTransitionExited?.(); - }} - > + <CSSTransition + nodeRef={containerRef} + in={open} + addEndListener={(done) => containerRef.current?.addEventListener('animationend', done)} + classNames={getCSSTransitionClassNames(b)} + mountOnEnter={!keepMounted} + unmountOnExit={!keepMounted} + appear={true} + onEnter={() => { + onTransitionEnter?.(); + }} + onEntered={() => { + onTransitionEntered?.(); + }} + onExit={() => { + onTransitionExit?.(); + }} + onExited={() => { + onTransitionExited?.(); + }} + > + <Portal container={container} disablePortal={disablePortal}> <div ref={handleRef} style={styles.popper} @@ -211,7 +209,7 @@ export function Popup({ </div> </FocusTrap> </div> - </CSSTransition> - </Portal> + </Portal> + </CSSTransition> ); } diff --git a/src/components/Portal/Portal.scss b/src/components/Portal/Portal.scss new file mode 100644 index 0000000000..58fa6954a2 --- /dev/null +++ b/src/components/Portal/Portal.scss @@ -0,0 +1,9 @@ +@use '../variables'; + +$block: '.#{variables.$ns}portal'; + +#{$block} { + &__theme-wrapper { + display: contents; + } +} diff --git a/src/components/Portal/Portal.tsx b/src/components/Portal/Portal.tsx index cbc0bdb7ea..28c3bcb8be 100644 --- a/src/components/Portal/Portal.tsx +++ b/src/components/Portal/Portal.tsx @@ -3,6 +3,13 @@ import React from 'react'; import ReactDOM from 'react-dom'; import {usePortalContainer} from '../../hooks'; +import {ThemeProvider} from '../theme'; +import {useThemeContext} from '../theme/useThemeContext'; +import {block} from '../utils/cn'; + +import './Portal.scss'; + +const b = block('portal'); export interface PortalProps { container?: HTMLElement; @@ -12,6 +19,7 @@ export interface PortalProps { export function Portal({container, children, disablePortal}: PortalProps) { const defaultContainer = usePortalContainer(); + const {scoped} = useThemeContext(); const containerNode = container ?? defaultContainer; @@ -19,5 +27,16 @@ export function Portal({container, children, disablePortal}: PortalProps) { return <React.Fragment>{children}</React.Fragment>; } - return containerNode ? ReactDOM.createPortal(children, containerNode) : null; + return containerNode + ? ReactDOM.createPortal( + scoped ? ( + <ThemeProvider rootClassName={b('theme-wrapper')} scoped> + {children} + </ThemeProvider> + ) : ( + children + ), + containerNode, + ) + : null; } diff --git a/src/components/layout/LayoutProvider/LayoutProvider.tsx b/src/components/layout/LayoutProvider/LayoutProvider.tsx index 37cc58fa07..33addad11e 100644 --- a/src/components/layout/LayoutProvider/LayoutProvider.tsx +++ b/src/components/layout/LayoutProvider/LayoutProvider.tsx @@ -4,7 +4,7 @@ import React from 'react'; import {LayoutContext} from '../contexts/LayoutContext'; import {useCurrentActiveMediaQuery} from '../hooks/useCurrentActiveMediaQuery'; import type {LayoutTheme, MediaType, RecursivePartial} from '../types'; -import {makeLayoutDefaultTheme} from '../utils/makeLayoutDefaultTheme'; +import {overrideLayoutTheme} from '../utils/overrideLayoutTheme'; export interface PrivateLayoutProviderProps { config?: RecursivePartial<LayoutTheme>; @@ -20,19 +20,15 @@ export function PrivateLayoutProvider({ config: override, initialMediaQuery, }: PrivateLayoutProviderProps) { - const theme = React.useMemo(() => makeLayoutDefaultTheme({override}), [override]); + const parentContext = React.useContext(LayoutContext); + const theme = React.useMemo( + () => overrideLayoutTheme({theme: parentContext.theme, override}), + [override, parentContext.theme], + ); const activeMediaQuery = useCurrentActiveMediaQuery(theme.breakpoints, initialMediaQuery); - return ( - <LayoutContext.Provider - value={{ - activeMediaQuery, - theme, - }} - > - {children} - </LayoutContext.Provider> - ); + const value = React.useMemo(() => ({activeMediaQuery, theme}), [activeMediaQuery, theme]); + return <LayoutContext.Provider value={value}>{children}</LayoutContext.Provider>; } interface LayoutProviderProps { diff --git a/src/components/layout/hooks/useCurrentActiveMediaQuery.tsx b/src/components/layout/hooks/useCurrentActiveMediaQuery.tsx index f58f4ed412..14be78a08d 100644 --- a/src/components/layout/hooks/useCurrentActiveMediaQuery.tsx +++ b/src/components/layout/hooks/useCurrentActiveMediaQuery.tsx @@ -25,12 +25,12 @@ export const makeCurrentActiveMediaExpressions = ( xxxl: `(min-width: ${mediaToValue.xxxl}px)`, }); -const safeMatchMedia = (query: string | number): MediaQueryList => { +const safeMatchMedia = (query: string): MediaQueryList => { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { return mockMediaQueryList; } - return window.matchMedia(String(query)); + return window.matchMedia(query); }; class Queries { @@ -81,33 +81,20 @@ export const useCurrentActiveMediaQuery = ( const [state, _setState] = React.useState<MediaType>(initialMediaQuery); React.useLayoutEffect(() => { - let mounted = true; - const queries = new Queries(breakpointsMap); const setState = () => { _setState(queries.getCurrentActiveMedia()); }; - const onChange = () => { - if (!mounted) { - return; - } - - setState(); - }; - - queries.addListeners(onChange); + queries.addListeners(setState); setState(); return () => { - mounted = false; - queries.removeListeners(onChange); + queries.removeListeners(setState); }; - // don't support runtime breakpoint redefinition. Breakpoints defined only one at LayoutTheme - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [breakpointsMap]); return state; }; diff --git a/src/components/layout/utils/makeLayoutDefaultTheme.ts b/src/components/layout/utils/makeLayoutDefaultTheme.ts deleted file mode 100644 index 7c20cdb14c..0000000000 --- a/src/components/layout/utils/makeLayoutDefaultTheme.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* eslint-disable valid-jsdoc */ -import merge from 'lodash/merge'; - -import {DEFAULT_LAYOUT_THEME} from '../constants'; -import type {LayoutTheme, RecursivePartial} from '../types'; - -interface MakeDefaultLayoutTheme { - override?: RecursivePartial<LayoutTheme>; -} - -/** - * Use this function to override default `DEFAULT_LAYOUT_THEME` - */ -export const makeLayoutDefaultTheme = ({ - override, -}: MakeDefaultLayoutTheme | undefined = {}): LayoutTheme => { - return merge(DEFAULT_LAYOUT_THEME, override); -}; diff --git a/src/components/layout/utils/overrideLayoutTheme.ts b/src/components/layout/utils/overrideLayoutTheme.ts new file mode 100644 index 0000000000..1cd6501bac --- /dev/null +++ b/src/components/layout/utils/overrideLayoutTheme.ts @@ -0,0 +1,16 @@ +/* eslint-disable valid-jsdoc */ +import merge from 'lodash/merge'; + +import type {LayoutTheme, RecursivePartial} from '../types'; + +interface OverrideLayoutThemeOptions { + theme: LayoutTheme; + override?: RecursivePartial<LayoutTheme>; +} + +/** + * Use this function to override default `DEFAULT_LAYOUT_THEME` + */ +export function overrideLayoutTheme({theme, override}: OverrideLayoutThemeOptions): LayoutTheme { + return merge(theme, override); +} diff --git a/src/components/theme/ThemeProvider.tsx b/src/components/theme/ThemeProvider.tsx index c982e03c1c..c0690affe0 100644 --- a/src/components/theme/ThemeProvider.tsx +++ b/src/components/theme/ThemeProvider.tsx @@ -80,8 +80,9 @@ export function ThemeProvider({ theme, themeValue, direction, + scoped, }) satisfies ThemeContextProps, - [theme, themeValue, direction], + [theme, themeValue, direction, scoped], ); const themeSettingsContext = React.useMemo( @@ -102,11 +103,7 @@ export function ThemeProvider({ }, rootClassName, )} - dir={ - hasParentProvider && direction === parentDirection - ? undefined - : direction - } + dir={direction} > {children} </div> diff --git a/src/components/theme/__stories__/Theme.stories.tsx b/src/components/theme/__stories__/Theme.stories.tsx index 38cd6ed0ac..b43b88ab24 100644 --- a/src/components/theme/__stories__/Theme.stories.tsx +++ b/src/components/theme/__stories__/Theme.stories.tsx @@ -3,7 +3,10 @@ import React from 'react'; import type {Meta, StoryObj} from '@storybook/react'; import {Button} from '../../Button'; +import {Dialog} from '../../Dialog'; +import {Select} from '../../Select'; import {Text} from '../../Text'; +import {Tooltip} from '../../Tooltip'; import {ThemeProvider} from '../ThemeProvider'; import {useDirection} from '../useDirection'; @@ -11,24 +14,9 @@ const meta: Meta<typeof ThemeProvider> = { title: 'Components/Utils/ThemeProvider', component: ThemeProvider, tags: ['nodocs'], - argTypes: { - theme: { - options: ['none', 'light', 'dark', 'light-hc', 'dark-hc', 'system'], - control: { - type: 'select', - }, - mapping: { - none: undefined, - }, - }, - direction: { - options: ['none', 'ltr', 'rtl'], - control: { - type: 'radio', - }, - mapping: { - none: undefined, - }, + parameters: { + controls: { + disable: true, }, }, }; @@ -38,22 +26,74 @@ export default meta; type Story = StoryObj<typeof ThemeProvider>; function ScopedComponent() { + const [open, setOpen] = React.useState(false); return ( - <div style={{transform: 'scaleX(var(--g-flow-direction))'}}> - <Button>{`current direction: ${useDirection()}`}</Button> + <div> + <Tooltip content="tooltip"> + <Button + onClick={() => { + setOpen(!open); + }} + >{`current direction: ${useDirection()}`}</Button> + </Tooltip> + <Dialog open={open} onClose={() => setOpen(false)}> + <Dialog.Header caption="Dialog.Header" /> + <Dialog.Body> + Dialog.Body + <Select> + <Select.Option value="one">One</Select.Option> + <Select.Option value="two">Two</Select.Option> + <Select.Option value="three">Three</Select.Option> + </Select> + </Dialog.Body> + </Dialog> </div> ); } export const Scoped: Story = { render: function ThemeScoped(props) { + const style: React.CSSProperties = { + border: '1px red dotted', + padding: 10, + height: '100%', + boxSizing: 'border-box', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'column', + gap: 10, + }; return ( - <div> - <ScopedComponent /> - - <ThemeProvider {...props}> - <div style={{border: '1px red dotted', padding: 10, marginBlockStart: 10}}> - <Text>Inside scoped theme provider</Text> + <div + style={{ + display: 'grid', + gridTemplateColumns: '1fr 1fr', + gridTemplateRows: '300px 300px', + gap: 10, + }} + > + <ThemeProvider {...props} theme="light" direction="rtl"> + <div style={style}> + <Text>Inside scoped theme provider (light)</Text> + <ScopedComponent /> + </div> + </ThemeProvider> + <ThemeProvider {...props} theme="dark"> + <div style={style}> + <Text>Inside scoped theme provider (dark)</Text> + <ScopedComponent /> + </div> + </ThemeProvider> + <ThemeProvider {...props} theme="light-hc"> + <div style={style}> + <Text>Inside scoped theme provider (light-hc)</Text> + <ScopedComponent /> + </div> + </ThemeProvider> + <ThemeProvider {...props} theme="dark-hc" direction="rtl"> + <div style={style}> + <Text>Inside scoped theme provider (dark-hc)</Text> <ScopedComponent /> </div> </ThemeProvider> diff --git a/src/components/theme/getDarkMediaMatch.ts b/src/components/theme/getDarkMediaMatch.ts index 5bdf5f7cfa..9099dc4aa2 100644 --- a/src/components/theme/getDarkMediaMatch.ts +++ b/src/components/theme/getDarkMediaMatch.ts @@ -1 +1,3 @@ +export const supportsMatchMedia = + typeof window !== 'undefined' && typeof window.matchMedia === 'function'; export const getDarkMediaMatch = () => window.matchMedia('(prefers-color-scheme: dark)'); diff --git a/src/components/theme/getSystemTheme.ts b/src/components/theme/getSystemTheme.ts index 10fc693bf5..19c35ce446 100644 --- a/src/components/theme/getSystemTheme.ts +++ b/src/components/theme/getSystemTheme.ts @@ -1,7 +1,7 @@ -import {getDarkMediaMatch} from './getDarkMediaMatch'; +import {getDarkMediaMatch, supportsMatchMedia} from './getDarkMediaMatch'; export function getSystemTheme() { - if (typeof window === 'object') { + if (supportsMatchMedia) { return getDarkMediaMatch().matches ? 'dark' : 'light'; } else { return 'light'; diff --git a/src/components/theme/types.ts b/src/components/theme/types.ts index 13940271b2..c3551f521b 100644 --- a/src/components/theme/types.ts +++ b/src/components/theme/types.ts @@ -8,4 +8,5 @@ export interface ThemeContextProps { theme: Theme; themeValue: RealTheme; direction: Direction; + scoped?: boolean; } diff --git a/src/components/theme/useSystemTheme.ts b/src/components/theme/useSystemTheme.ts index a2fe53b2dd..8deca3e2bf 100644 --- a/src/components/theme/useSystemTheme.ts +++ b/src/components/theme/useSystemTheme.ts @@ -1,6 +1,6 @@ import React from 'react'; -import {getDarkMediaMatch} from './getDarkMediaMatch'; +import {getDarkMediaMatch, supportsMatchMedia} from './getDarkMediaMatch'; import {getSystemTheme} from './getSystemTheme'; import type {ThemeType} from './types'; @@ -29,6 +29,10 @@ export function useSystemTheme(): ThemeType { const [theme, setTheme] = React.useState<ThemeType>(getSystemTheme()); React.useEffect(() => { + if (!supportsMatchMedia) { + return undefined; + } + function onChange(event: MediaQueryListEvent) { setTheme(event.matches ? 'dark' : 'light'); } From 02bf33c1cb277e29866c86c4e7264b618feebfe1 Mon Sep 17 00:00:00 2001 From: Isaev Alexandr <aisaev188@yandex-team.ru> Date: Wed, 24 Apr 2024 18:36:02 +0300 Subject: [PATCH 21/24] fix(useList): remove debug dev only information (#1552) --- src/components/useList/utils/defaultFilterItems.ts | 7 ------- src/components/useList/utils/flattenItems.ts | 7 ------- src/components/useList/utils/getListParsedState.ts | 8 -------- 3 files changed, 22 deletions(-) diff --git a/src/components/useList/utils/defaultFilterItems.ts b/src/components/useList/utils/defaultFilterItems.ts index 5e3adc0d9b..35b5bda998 100644 --- a/src/components/useList/utils/defaultFilterItems.ts +++ b/src/components/useList/utils/defaultFilterItems.ts @@ -6,10 +6,6 @@ export function defaultFilterItems<T>( items: ListItemType<T>[], filterFn: (data: T) => boolean, ): ListItemType<T>[] { - if (process.env.NODE_ENV !== 'production') { - console.time('defaultFilterItems'); - } - const getChildren = (result: ListItemType<T>[], item: ListItemType<T>) => { if (isTreeItemGuard(item) && item.children) { const children = item.children.reduce(getChildren, []); @@ -31,8 +27,5 @@ export function defaultFilterItems<T>( const res = items.reduce<ListItemType<T>[]>(getChildren, []); - if (process.env.NODE_ENV !== 'production') { - console.timeEnd('defaultFilterItems'); - } return res; } diff --git a/src/components/useList/utils/flattenItems.ts b/src/components/useList/utils/flattenItems.ts index 54fb0e98de..8c013ed556 100644 --- a/src/components/useList/utils/flattenItems.ts +++ b/src/components/useList/utils/flattenItems.ts @@ -9,10 +9,6 @@ export function flattenItems<T>( expandedById: Record<ListItemId, boolean> = {}, getItemId?: (item: T) => ListItemId, ): ParsedFlattenState { - if (process.env.NODE_ENV !== 'production') { - console.time('flattenItems'); - } - const getNestedIds = ( order: string[], item: ListItemType<T>, @@ -50,9 +46,6 @@ export function flattenItems<T>( idToFlattenIndex[index] = item; } - if (process.env.NODE_ENV !== 'production') { - console.timeEnd('flattenItems'); - } return { visibleFlattenIds, idToFlattenIndex, diff --git a/src/components/useList/utils/getListParsedState.ts b/src/components/useList/utils/getListParsedState.ts index e675209966..8b26c0d0d8 100644 --- a/src/components/useList/utils/getListParsedState.ts +++ b/src/components/useList/utils/getListParsedState.ts @@ -39,10 +39,6 @@ export function getListParsedState<T>( */ getItemId?: (item: T) => ListItemId, ): ListParsedStateResult<T> { - if (process.env.NODE_ENV !== 'production') { - console.time('getListParsedState'); - } - const result: ListParsedStateResult<T> = { itemsById: {}, groupsState: {}, @@ -137,9 +133,5 @@ export function getListParsedState<T>( isTreeItemGuard(item) ? traverseTreeItem({item, index}) : traverseItem({item, index}), ); - if (process.env.NODE_ENV !== 'production') { - console.timeEnd('getListParsedState'); - } - return result; } From 575477a1901d3dbd4c84d8320bad5a0201b6c589 Mon Sep 17 00:00:00 2001 From: Isaev Alexandr <aisaev188@yandex-team.ru> Date: Wed, 24 Apr 2024 18:42:40 +0300 Subject: [PATCH 22/24] fix(layout): update layout docs (#1550) --- src/components/layout/LayoutProvider/__stories__/Layout.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/layout/LayoutProvider/__stories__/Layout.mdx b/src/components/layout/LayoutProvider/__stories__/Layout.mdx index 3306d2eaf4..e25eadbd8b 100644 --- a/src/components/layout/LayoutProvider/__stories__/Layout.mdx +++ b/src/components/layout/LayoutProvider/__stories__/Layout.mdx @@ -76,13 +76,13 @@ _You can override default values on project level:_ ```tsx import {ThemeProvider, LayoutTheme} from '@gravity-ui/uikit'; -const theme: LayoutTheme = { +const config: LayoutTheme = { spaceBaseSize: 5, }; export const App = () => { return ( - <ThemeProvider layout={{theme}}> + <ThemeProvider layout={{config}}> {...} </ThemeProvider> ); @@ -138,7 +138,7 @@ We use `mobile-first` approach. It means that you should adapt you app to deskto } }; - <ThemeProvider layout={{theme: APP_LAYOUT_THEME}}> + <ThemeProvider layout={{config: APP_LAYOUT_THEME}}> {...} </ThemeProvider> ``` From 3a464218a54f524f490f91cbe5a1c3d6132b44e6 Mon Sep 17 00:00:00 2001 From: Taya Leutina <leutinatasya@yandex-team.ru> Date: Thu, 25 Apr 2024 19:29:32 +0300 Subject: [PATCH 23/24] fix(Select): fix examples in docs and storybook (#1553) --- src/components/Select/README.md | 13 +++--- .../__stories__/SelectPopupWidthShowcase.tsx | 18 +++++++-- .../Select/__stories__/SelectShowcase.scss | 4 ++ .../Select/__stories__/SelectShowcase.tsx | 40 +++++++------------ .../Select/__stories__/constants.ts | 27 +++++++++++++ 5 files changed, 67 insertions(+), 35 deletions(-) diff --git a/src/components/Select/README.md b/src/components/Select/README.md index 8d7146ee18..d60f3ed295 100644 --- a/src/components/Select/README.md +++ b/src/components/Select/README.md @@ -1023,11 +1023,12 @@ To render custom popup use the `renderPopup` property. code={` <Select filterable - renderSelectedOption={({renderList, renderFilter}) => { + placeholder="Custom popup" + renderPopup={({renderList, renderFilter}) => { return ( <React.Fragment> {renderFilter()} - <div className="CustomElement" /> + <div style={{width: "100%", height: "20px", backgroundColor: "tomato"}} /> {renderList()} </React.Fragment> ); @@ -1042,16 +1043,16 @@ To render custom popup use the `renderPopup` property. > <UIKit.Select filterable - placeholder="Custom selected options" - renderSelectedOption={({renderList, renderFilter}) => { + placeholder="Custom popup" + renderPopup={({renderList, renderFilter}) => { return ( <React.Fragment> {renderFilter()} <div style={{width: "100%", height: "20px", backgroundColor: "tomato"}} /> {renderList()} </React.Fragment> - ); - }} + ); +}} > <UIKit.Select.Option value="val_1" data={{color: '#8FE1A1'}}>Value 1</UIKit.Select.Option> <UIKit.Select.Option value="val_2" data={{color: '#38C0A8'}}>Value 2</UIKit.Select.Option> diff --git a/src/components/Select/__stories__/SelectPopupWidthShowcase.tsx b/src/components/Select/__stories__/SelectPopupWidthShowcase.tsx index 25f990203e..e2745df246 100644 --- a/src/components/Select/__stories__/SelectPopupWidthShowcase.tsx +++ b/src/components/Select/__stories__/SelectPopupWidthShowcase.tsx @@ -24,7 +24,7 @@ export const SelectPopupWidthShowcase = (args: any) => { <h2>Static width (props.popupWidth)</h2> <Select {...args} - className="select-width-50" + className="select-example select-width-50" options={extralong} popupWidth={200} title="Sample select" @@ -34,12 +34,24 @@ export const SelectPopupWidthShowcase = (args: any) => { options={extralong} width="max" popupWidth={200} + className="select-example" title="Sample select" /> <h2>Short values. Adjust to Control width</h2> - <Select {...args} className="select-width-300" options={short} title="Sample select" /> - <Select {...args} options={short} width="max" title="Sample select" /> + <Select + {...args} + className="select-example select-width-300" + options={short} + title="Sample select" + /> + <Select + {...args} + className="select-example" + options={short} + width="max" + title="Sample select" + /> <h2>Modes default/fit</h2> <Flex gap={10}> diff --git a/src/components/Select/__stories__/SelectShowcase.scss b/src/components/Select/__stories__/SelectShowcase.scss index f2705e3a6c..794f3382f2 100644 --- a/src/components/Select/__stories__/SelectShowcase.scss +++ b/src/components/Select/__stories__/SelectShowcase.scss @@ -11,6 +11,10 @@ $blockSelectClear: '.#{variables.$ns}select-clear'; width: 300px; } +.select-example { + margin: 5px 0; +} + #{$block} { display: flex; flex-direction: column; diff --git a/src/components/Select/__stories__/SelectShowcase.tsx b/src/components/Select/__stories__/SelectShowcase.tsx index 3ea86c4d00..b4bb839588 100644 --- a/src/components/Select/__stories__/SelectShowcase.tsx +++ b/src/components/Select/__stories__/SelectShowcase.tsx @@ -17,6 +17,7 @@ import {block} from '../../utils/cn'; import { EXAMPLE_CHILDREN_OPTIONS, EXAMPLE_CUSTOM_FILTER_SECTION, + EXAMPLE_CUSTOM_POPUP, EXAMPLE_CUSTOM_RENDERER_WITH_DISABLED_ITEM, EXAMPLE_DISABLED_OPTIONS, EXAMPLE_GROUP_CHILDREN_OPTIONS, @@ -366,10 +367,21 @@ export const SelectShowcase = (props: SelectProps) => { </ExampleItem> <ExampleItem title="Select with custom popup" - code={[EXAMPLE_JSON_OPTIONS, EXAMPLE_CHILDREN_OPTIONS]} + code={[EXAMPLE_CUSTOM_POPUP]} selectProps={{ ...props, - popupClassName: b('custom-popup'), + filterable: true, + renderPopup: ({renderFilter, renderList}) => { + return ( + <React.Fragment> + <div>{'---- Before Filter ----'}</div> + {renderFilter()} + <div>{'---- After Filter, Before List ----'}</div> + {renderList()} + <div>{'---- After List ----'}</div> + </React.Fragment> + ); + }, }} > <Select.Option value="val1" content="Value1" /> @@ -411,30 +423,6 @@ export const SelectShowcase = (props: SelectProps) => { <Select.Option value="val4" content="Value4" /> </ExampleItem> </div> - - <ExampleItem - title="Select with custom popup" - selectProps={{ - ...props, - filterable: true, - renderPopup: ({renderFilter, renderList}) => { - return ( - <React.Fragment> - <div>{'---- Before Filter ----'}</div> - {renderFilter()} - <div>{'---- After Filter, Before List ----'}</div> - {renderList()} - <div>{'---- After List ----'}</div> - </React.Fragment> - ); - }, - }} - > - <Select.Option value="val1" content="Value1" /> - <Select.Option value="val2" content="Value2" /> - <Select.Option value="val3" content="Value3" /> - <Select.Option value="val4" content="Value4" /> - </ExampleItem> </div> ); }; diff --git a/src/components/Select/__stories__/constants.ts b/src/components/Select/__stories__/constants.ts index 7db104710d..94f495b465 100644 --- a/src/components/Select/__stories__/constants.ts +++ b/src/components/Select/__stories__/constants.ts @@ -285,3 +285,30 @@ const renderFilter: SelectProps['renderFilter'] = ({value, ref, onChange, onKeyD </Select> `; + +export const EXAMPLE_CUSTOM_POPUP = `const [value, setValue] = React.useState<string[]>([]); + +<Select + value={value} + placeholder="Values", + onUpdate={(nextValue) => setValue(nextValue)} + filterable: true, + renderPopup: ({renderFilter, renderList}) => { + return ( + <React.Fragment> + <div>{'---- Before Filter ----'}</div> + {renderFilter()} + <div>{'---- After Filter, Before List ----'}</div> + {renderList()} + <div>{'---- After List ----'}</div> + </React.Fragment> + ); + }, +> + <Select.Option value="val1" content="Value1" /> + <Select.Option value="val2" content="Value2" /> + <Select.Option value="val3" content="Value3" /> + <Select.Option value="val4" content="Value4" /> +</Select> + +`; From 9e93c917e3aa13b262dc3c33b960fa39e84902b7 Mon Sep 17 00:00:00 2001 From: Artur Abdullin <30603428+ArturAbdullin@users.noreply.github.com> Date: Fri, 26 Apr 2024 14:24:36 +0300 Subject: [PATCH 24/24] feat(withTableSettings): add a reset action (#1526) --- .../Table/__stories__/Table.stories.tsx | 40 +++++++++++------ .../TableColumnSetup/TableColumnSetup.tsx | 29 +++++++++++- .../TableColumnSetup/i18n/en.json | 5 ++- .../TableColumnSetup/i18n/ru.json | 5 ++- .../withTableSettings/withTableSettings.tsx | 44 ++++++++++++++++++- 5 files changed, 102 insertions(+), 21 deletions(-) diff --git a/src/components/Table/__stories__/Table.stories.tsx b/src/components/Table/__stories__/Table.stories.tsx index 94265433e1..13062ec13f 100644 --- a/src/components/Table/__stories__/Table.stories.tsx +++ b/src/components/Table/__stories__/Table.stories.tsx @@ -4,6 +4,7 @@ import {Pencil} from '@gravity-ui/icons'; import {action} from '@storybook/addon-actions'; import type {Meta, StoryFn} from '@storybook/react'; import _cloneDeep from 'lodash/cloneDeep'; +import _isEqual from 'lodash/isEqual'; import type {TableAction, TableSettingsData} from '..'; import {Icon} from '../../Icon'; @@ -199,27 +200,17 @@ const WithTableSelectionTemplate: StoryFn<TableProps<DataItem>> = (args) => { }; export const HOCWithTableSelection = WithTableSelectionTemplate.bind({}); +const DEFAULT_SETTINGS = columns.map((x) => ({id: x.id, isSelected: true})); // --------------------------------- const WithTableSettingsTemplate: StoryFn<TableProps<DataItem>> = (args, context) => { - const [settings, setSettings] = React.useState<TableSettingsData>(() => - columns.map((x) => ({id: x.id, isSelected: true})), - ); - - const updateSettings = React.useCallback( - async (updatedSettings: TableSettingsData) => setSettings(updatedSettings), - [], - ); + const [settings, setSettings] = React.useState<TableSettingsData>(DEFAULT_SETTINGS); if (context.parameters.isFactory) { return ( - <TableWithSettingsFactory - {...args} - settings={settings} - updateSettings={updateSettings} - /> + <TableWithSettingsFactory {...args} settings={settings} updateSettings={setSettings} /> ); } else { - return <TableWithSettings {...args} settings={settings} updateSettings={updateSettings} />; + return <TableWithSettings {...args} settings={settings} updateSettings={setSettings} />; } }; export const HOCWithTableSettings = WithTableSettingsTemplate.bind({}); @@ -251,6 +242,27 @@ HOCWithTableSettingsFactory.parameters = { disableStrictMode: true, }; +const WithTableSettingsWithResetTemplate: StoryFn<TableProps<DataItem>> = (args) => { + const [settings, setSettings] = React.useState<TableSettingsData>(DEFAULT_SETTINGS); + + return ( + <TableWithSettings + {...args} + settings={settings} + updateSettings={setSettings} + defaultSettings={DEFAULT_SETTINGS} + showResetButton={!_isEqual(DEFAULT_SETTINGS, settings)} + /> + ); +}; + +export const HOCWithTableSettingsWithReset = WithTableSettingsWithResetTemplate.bind({}); +HOCWithTableSettingsWithReset.parameters = { + // Strict mode ruins sortable list due to this react-beautiful-dnd issue + // https://github.com/atlassian/react-beautiful-dnd/issues/2350 + disableStrictMode: true, +}; + const WithTableSettingsCustomActionsTemplate: StoryFn<TableProps<DataItem>> = (args) => { const settings = React.useMemo(() => { const newSettings: TableSettingsData = columns.map((x) => ({ diff --git a/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx b/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx index 08c170f7be..4f01d1ffa2 100644 --- a/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx +++ b/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx @@ -20,6 +20,7 @@ import type { TreeSelectRenderContainer, TreeSelectRenderItem, } from '../../../../TreeSelect/types'; +import {Flex} from '../../../../layout/Flex/Flex'; import type {ListItemCommonProps, ListItemViewProps} from '../../../../useList'; import {ListContainerView, ListItemView} from '../../../../useList'; import {block} from '../../../../utils/cn'; @@ -265,6 +266,9 @@ export interface TableColumnSetupProps { renderControls?: RenderControls; className?: string; + + defaultItems?: TableColumnSetupItem[]; + showResetButton?: boolean | ((currentItems: TableColumnSetupItem[]) => boolean); } export const TableColumnSetup = (props: TableColumnSetupProps) => { @@ -277,6 +281,8 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => { sortable, renderControls, className, + defaultItems = propsItems, + showResetButton: propsShowResetButton, } = props; const [open, setOpen] = React.useState(false); @@ -309,10 +315,31 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => { } }; + const showResetButton = + typeof propsShowResetButton === 'function' + ? propsShowResetButton(items) + : propsShowResetButton; + const dndRenderContainer = useDndRenderContainer({ onDragEnd, renderControls: () => - renderControls ? renderControls({DefaultApplyButton, onApply}) : <DefaultApplyButton />, + renderControls ? ( + renderControls({DefaultApplyButton, onApply}) + ) : ( + <Flex gapRow={1} direction="column" className={controlsCn}> + {showResetButton && ( + <Button + onClick={() => { + setItems(defaultItems); + }} + width="max" + > + {i18n('button_reset')} + </Button> + )} + <DefaultApplyButton /> + </Flex> + ), }); const dndRenderItem = useDndRenderItem(sortable); diff --git a/src/components/Table/hoc/withTableSettings/TableColumnSetup/i18n/en.json b/src/components/Table/hoc/withTableSettings/TableColumnSetup/i18n/en.json index 7cfe4b8cb7..2ff8659b5f 100644 --- a/src/components/Table/hoc/withTableSettings/TableColumnSetup/i18n/en.json +++ b/src/components/Table/hoc/withTableSettings/TableColumnSetup/i18n/en.json @@ -1,4 +1,5 @@ { - "button_switcher": "Columns", - "button_apply": "Apply" + "button_apply": "Apply", + "button_reset": "Reset", + "button_switcher": "Columns" } diff --git a/src/components/Table/hoc/withTableSettings/TableColumnSetup/i18n/ru.json b/src/components/Table/hoc/withTableSettings/TableColumnSetup/i18n/ru.json index e216dd735f..8b2daea582 100644 --- a/src/components/Table/hoc/withTableSettings/TableColumnSetup/i18n/ru.json +++ b/src/components/Table/hoc/withTableSettings/TableColumnSetup/i18n/ru.json @@ -1,4 +1,5 @@ { - "button_switcher": "Колонки", - "button_apply": "Применить" + "button_apply": "Применить", + "button_reset": "Сбросить", + "button_switcher": "Колонки" } diff --git a/src/components/Table/hoc/withTableSettings/withTableSettings.tsx b/src/components/Table/hoc/withTableSettings/withTableSettings.tsx index 4af72b02ac..68871c0adf 100644 --- a/src/components/Table/hoc/withTableSettings/withTableSettings.tsx +++ b/src/components/Table/hoc/withTableSettings/withTableSettings.tsx @@ -2,6 +2,7 @@ import React from 'react'; import {Gear} from '@gravity-ui/icons'; import _get from 'lodash/get'; +import _isEqual from 'lodash/isEqual'; import _isString from 'lodash/isString'; import _last from 'lodash/last'; @@ -111,7 +112,7 @@ export interface WithTableSettingsOptions { sortable?: boolean; } -export interface WithTableSettingsProps { +interface WithTableSettingsBaseProps { /** * @deprecated Use factory notation: "withTableSettings({width: <value>})(Table)" */ @@ -126,6 +127,25 @@ export interface WithTableSettingsProps { renderControls?: RenderControls; } +interface WithDefaultSettings { + /** Settings to which you can reset the current settings. */ + defaultSettings: TableSettingsData; + /** + * Display a reset button that resets the current settings changes. + * + * If the `defaultSettings` prop is set then the settings reset to the `defaultSettings`. + */ + showResetButton: boolean; +} + +interface WithoutDefaultSettings { + defaultSettings?: never; + showResetButton?: boolean; +} + +export type WithTableSettingsProps = WithTableSettingsBaseProps & + (WithDefaultSettings | WithoutDefaultSettings); + const b = block('table'); const POPUP_PLACEMENT: PopperPlacement = ['bottom-end', 'bottom', 'top-end', 'top', 'auto']; @@ -157,8 +177,18 @@ export function withTableSettings<I extends TableDataItem, E extends {} = {}>( columns, settingsPopupWidth, renderControls, + defaultSettings, + showResetButton, ...restTableProps }: TableProps<I> & WithTableSettingsProps & E) { + const defaultActualItems = React.useMemo(() => { + if (!defaultSettings) { + return undefined; + } + + return getActualItems(columns, defaultSettings); + }, [columns, defaultSettings]); + const enhancedColumns = React.useMemo(() => { const actualItems = getActualItems(columns, settings || []); @@ -182,11 +212,21 @@ export function withTableSettings<I extends TableDataItem, E extends {} = {}>( </Button> )} renderControls={renderControls} + defaultItems={defaultActualItems} + showResetButton={showResetButton} /> </div> ); }); - }, [columns, settings, updateSettings, settingsPopupWidth, renderControls]); + }, [ + columns, + settings, + settingsPopupWidth, + updateSettings, + renderControls, + defaultActualItems, + showResetButton, + ]); return ( <React.Fragment>