diff --git a/jest.setup.ts b/jest.setup.ts index 7b0828bfa..b6dab8349 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -1 +1,7 @@ import '@testing-library/jest-dom'; + +import ResizeObserver from './__mocks__/ResizeObserver'; + +jest.mock('resize-observer-polyfill', () => { + return ResizeObserver; +}); diff --git a/src/components/BasicSelectDeprecated/__tests__/BasicSelect.test.tsx b/src/components/BasicSelectDeprecated/__tests__/BasicSelect.test.tsx index 13b07e861..904f9727d 100644 --- a/src/components/BasicSelectDeprecated/__tests__/BasicSelect.test.tsx +++ b/src/components/BasicSelectDeprecated/__tests__/BasicSelect.test.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { act, fireEvent, render, RenderResult, screen } from '@testing-library/react'; -import ResizeObserver from '../../../../__mocks__/ResizeObserver'; import { cnSelect } from '../../SelectComponentsDeprecated/cnSelect'; import { cnSelectItem } from '../../SelectComponentsDeprecated/SelectItem/SelectItem'; import { BasicSelect, SimpleSelectProps } from '../BasicSelect'; @@ -14,10 +13,6 @@ type SelectOption = { const animationDuration = 200; const testId = 'BasicSelect'; -jest.mock('resize-observer-polyfill', () => { - return ResizeObserver; -}); - const items = [ { label: 'Neptunium', value: 'Neptunium' }, { label: 'Plutonium', value: 'Plutonium' }, diff --git a/src/components/Combobox/__tests__/Combobox.test.tsx b/src/components/Combobox/__tests__/Combobox.test.tsx index 167440ef7..2431a447e 100644 --- a/src/components/Combobox/__tests__/Combobox.test.tsx +++ b/src/components/Combobox/__tests__/Combobox.test.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { act, fireEvent, render, RenderResult, screen } from '@testing-library/react'; import { groups, items } from '../__mocks__/data.mock'; -import ResizeObserver from '../../../../__mocks__/ResizeObserver'; import { cn } from '../../../utils/bem'; import { cnSelect } from '../../SelectComponents/cnSelect'; import { cnSelectGroupLabel } from '../../SelectComponents/SelectGroupLabel/SelectGroupLabel'; @@ -11,10 +10,6 @@ import { cnSelectValueTag } from '../../SelectComponents/SelectValueTag/SelectVa import { Combobox, ComboboxProps, defaultGetItemLabel } from '../Combobox'; import { DefaultGroup, DefaultItem } from '../helpers'; -jest.mock('resize-observer-polyfill', () => { - return ResizeObserver; -}); - const animationDuration = 200; const testId = 'Combobox'; const cnRenderValue = cn('RenderValue'); diff --git a/src/components/ComboboxDeprecated/__tests__/Combobox.test.tsx b/src/components/ComboboxDeprecated/__tests__/Combobox.test.tsx index 821548224..74a56ee2f 100644 --- a/src/components/ComboboxDeprecated/__tests__/Combobox.test.tsx +++ b/src/components/ComboboxDeprecated/__tests__/Combobox.test.tsx @@ -2,15 +2,10 @@ import React, { useState } from 'react'; import { act, fireEvent, render, screen } from '@testing-library/react'; import { simpleItems } from '../__mocks__/data.mock'; -import ResizeObserver from '../../../../__mocks__/ResizeObserver'; import { cnSelect } from '../../SelectComponentsDeprecated/cnSelect'; import { cnSelectItem } from '../../SelectComponentsDeprecated/SelectItem/SelectItem'; import { Combobox } from '../Combobox'; -jest.mock('resize-observer-polyfill', () => { - return ResizeObserver; -}); - const testId = 'Combobox'; const animationDuration = 200; diff --git a/src/components/ContextMenu/__tests__/ContextMenu.test.tsx b/src/components/ContextMenu/__tests__/ContextMenu.test.tsx index 550093fee..c2a0186a9 100644 --- a/src/components/ContextMenu/__tests__/ContextMenu.test.tsx +++ b/src/components/ContextMenu/__tests__/ContextMenu.test.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { fireEvent, render, screen } from '@testing-library/react'; import { exampleItems as items, groups, Item } from '../__mocks__/mock.data'; -import ResizeObserver from '../../../../__mocks__/ResizeObserver'; import { cnText } from '../../Text/Text'; import { ContextMenu } from '../ContextMenu'; import { cnContextMenuGroupHeader } from '../ContextMenuGroupHeader/ContextMenuGroupHeader'; @@ -10,10 +9,6 @@ import { cnContextMenuItem } from '../ContextMenuItem/ContextMenuItem'; import { cnContextMenuLevel } from '../ContextMenuLevel/ContextMenuLevel'; import { ContextMenuProps } from '../helpers'; -jest.mock('resize-observer-polyfill', () => { - return ResizeObserver; -}); - const testId = 'ContextMenu'; const additionalClass = 'additionalClass'; diff --git a/src/components/MultiComboboxDeprecated/__tests__/MultiCombobox.test.tsx b/src/components/MultiComboboxDeprecated/__tests__/MultiCombobox.test.tsx index dc8ec4426..ddd1f5b16 100644 --- a/src/components/MultiComboboxDeprecated/__tests__/MultiCombobox.test.tsx +++ b/src/components/MultiComboboxDeprecated/__tests__/MultiCombobox.test.tsx @@ -1,16 +1,11 @@ import React, { useState } from 'react'; import { act, fireEvent, render, screen } from '@testing-library/react'; -import ResizeObserver from '../../../../__mocks__/ResizeObserver'; import { simpleItems } from '../../ComboboxDeprecated/__mocks__/data.mock'; import { cnSelect } from '../../SelectComponentsDeprecated/cnSelect'; import { cnSelectItem } from '../../SelectComponentsDeprecated/SelectItem/SelectItem'; import { MultiCombobox } from '../MultiCombobox'; -jest.mock('resize-observer-polyfill', () => { - return ResizeObserver; -}); - const testId = 'MultiCombobox'; const animationDuration = 200; diff --git a/src/components/Select/__tests__/Select.test.tsx b/src/components/Select/__tests__/Select.test.tsx index ad0ad516d..f5851d5cc 100644 --- a/src/components/Select/__tests__/Select.test.tsx +++ b/src/components/Select/__tests__/Select.test.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { act, fireEvent, render, RenderResult, screen } from '@testing-library/react'; import { groups, items } from '../__mocks__/data.mock'; -import ResizeObserver from '../../../../__mocks__/ResizeObserver'; import { cn } from '../../../utils/bem'; import { cnSelect } from '../../SelectComponents/cnSelect'; import { cnSelectGroupLabel } from '../../SelectComponents/SelectGroupLabel/SelectGroupLabel'; @@ -14,10 +13,6 @@ const testId = 'Select'; const cnRenderValue = cn('RenderValue'); const cnRenderItem = cn('RenderItem'); -jest.mock('resize-observer-polyfill', () => { - return ResizeObserver; -}); - const defaultProps: SelectProps = { items, groups, diff --git a/src/components/Table/__tests__/Table.test.tsx b/src/components/Table/__tests__/Table.test.tsx index 6c74431fc..46b091b21 100644 --- a/src/components/Table/__tests__/Table.test.tsx +++ b/src/components/Table/__tests__/Table.test.tsx @@ -1,13 +1,8 @@ import * as React from 'react'; import { render, screen } from '@testing-library/react'; -import ResizeObserver from '../../../../__mocks__/ResizeObserver'; import { Props, Table } from '../Table'; -jest.mock('resize-observer-polyfill', () => { - return ResizeObserver; -}); - const rows = [ { id: 'row1', diff --git a/src/components/Tabs/Tabs.css b/src/components/Tabs/Tabs.css index a3786f442..8223bfe77 100644 --- a/src/components/Tabs/Tabs.css +++ b/src/components/Tabs/Tabs.css @@ -40,46 +40,14 @@ } &-RunningLine { - left: -1px; - width: 1px; - border-radius: 1px 0 0 0; - transition: opacity 0.2s, transform 0.25s; - transform: translateX(var(--tabOffsetLeft, 0)); - - &, - &::before, - &::after { - position: absolute; - bottom: 0; - height: 2px; - background-color: var(--color-bg-brand); - transform-origin: left center; - } - - &::before { - content: ''; - left: 1px; - width: var(--tabsWidth); - transition: transform 0.25s; - transform: scaleX(var(--tabRatio, 0.0001)); - } - - &::after { - content: ''; - left: 1px; - width: 1px; - border-radius: 0 1px 0 0; - transition: transform 0.25s; - transform: translateX(var(--tabWidth)); - } - - &_withOutValue { - opacity: 0; - } - } - - &-WrapperRunningLine { - overflow: hidden; - width: 100%; + position: absolute; + left: 0; + bottom: 0; + width: var(--tabSize); + height: 2px; + background-color: var(--color-bg-brand); + transition: transform 0.25s, width 0.25s; + transform: translateX(var(--tabOffset)); + transform-origin: left center; } } diff --git a/src/components/Tabs/Tabs.tsx b/src/components/Tabs/Tabs.tsx index ae1c44503..9a1268634 100644 --- a/src/components/Tabs/Tabs.tsx +++ b/src/components/Tabs/Tabs.tsx @@ -1,9 +1,9 @@ import './Tabs.css'; -import React, { createRef, useEffect, useMemo, useRef } from 'react'; +import React, { createRef, useMemo } from 'react'; import { useChoiceGroup } from '../../hooks/useChoiceGroup/useChoiceGroup'; -import { useForkRef } from '../../hooks/useForkRef/useForkRef'; +import { useResizeObserved } from '../../hooks/useResizeObserved/useResizeObserved'; import { IconProps, IconPropSize } from '../../icons/Icon/Icon'; import { cn } from '../../utils/bem'; import { getSizeByMap } from '../../utils/getSizeByMap'; @@ -75,22 +75,6 @@ const sizeMap: Record = { m: 's', }; -function setStyleForLine( - lineRef: React.RefObject, - tabsWidth: number, - tabWidth: number, - tabRatio: number, - tabOffsetLeft: number, -) { - if (lineRef.current) { - const lineStyle = lineRef.current.style; - lineStyle.setProperty('--tabsWidth', `${tabsWidth}px`); - lineStyle.setProperty('--tabWidth', `${tabWidth}px`); - lineStyle.setProperty('--tabRatio', `${tabRatio}`); - lineStyle.setProperty('--tabOffsetLeft', `${tabOffsetLeft}px`); - } -} - function renderItemDefault( props: RenderItemProps, ): React.ReactElement { @@ -129,51 +113,26 @@ export const Tabs: Tabs = React.forwardRef((props, ref) => { multiple: false, }); - const constructItemRefs: () => Record> = () => { - const refs: Record> = {}; - for (const item of items) { - refs[getLabel(item)] = createRef(); - } - return refs; - }; - - const buttonRefs = useMemo(constructItemRefs, [items, getLabel]); - - const rootRef = useRef(null); - const lineRef = useRef(null); - - const updateLine = () => { - if (rootRef.current && lineRef.current && buttonRefs) { - const rootWidth = rootRef.current.offsetWidth; - if (value) { - const activeItemRef = buttonRefs[getLabel(value)]; - if (activeItemRef && activeItemRef.current) { - const itemWidth = activeItemRef.current.offsetWidth; - const itemOffsetLeft = activeItemRef.current.offsetLeft; - setStyleForLine(lineRef, rootWidth, itemWidth, itemWidth / rootWidth, itemOffsetLeft); - } - } else { - setStyleForLine(lineRef, rootWidth, 1, 0.00001, 1); - } - } - }; - - useEffect(() => updateLine()); - - const withOutValue = !value; + const tabRefs = useMemo( + () => new Array(items.length).fill(null).map(() => createRef()), + [items], + ); + const tabsDimensions = useResizeObserved(tabRefs, (el) => ({ + size: el?.offsetWidth ?? 0, + offset: el?.offsetLeft ?? 0, + })); + const activeTabIdx = (value && items.indexOf(value)) ?? -1; + const activeTabDimensions = tabsDimensions[activeTabIdx]; + const iconSize = getSizeByMap(sizeMap, size, iconSizeProp); return ( -
([ref, rootRef])} - {...otherProps} - > +
- {items.map((item) => + {items.map((item, idx) => renderItem({ item, - ref: buttonRefs[getLabel(item)], + ref: tabRefs[idx], key: getLabel(item), onChange: getOnChange(item), checked: getChecked(item), @@ -185,9 +144,15 @@ export const Tabs: Tabs = React.forwardRef((props, ref) => { }), )}
-
-
-
+ {activeTabDimensions?.size > 0 && ( +
+ )}
); }); diff --git a/src/components/ThemeToggler/__tests__/ThemeToggler.test.tsx b/src/components/ThemeToggler/__tests__/ThemeToggler.test.tsx index dbfdc15a7..210ae0f14 100644 --- a/src/components/ThemeToggler/__tests__/ThemeToggler.test.tsx +++ b/src/components/ThemeToggler/__tests__/ThemeToggler.test.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { fireEvent, render, screen } from '@testing-library/react'; import { exampleThemesThree, exampleThemesTwo } from '../__mocks__/data.mock'; -import ResizeObserver from '../../../../__mocks__/ResizeObserver'; import { cnContextMenuItem } from '../../ContextMenu/ContextMenuItem/ContextMenuItem'; import { Props, ThemeToggler } from '../ThemeToggler'; @@ -13,10 +12,6 @@ type ThemeTogglerProps = Props; const defaultSetValue = jest.fn(); const testId = 'ThemeToggler'; -jest.mock('resize-observer-polyfill', () => { - return ResizeObserver; -}); - const renderComponent = (props: Partial) => { return render( <> diff --git a/src/components/UserSelect/__tests__/UserSelect.test.tsx b/src/components/UserSelect/__tests__/UserSelect.test.tsx index 5d3f42a24..06c7acae0 100644 --- a/src/components/UserSelect/__tests__/UserSelect.test.tsx +++ b/src/components/UserSelect/__tests__/UserSelect.test.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { act, fireEvent, render, RenderResult, screen } from '@testing-library/react'; import { groups, items } from '../__mocks__/data.mock'; -import ResizeObserver from '../../../../__mocks__/ResizeObserver'; import { cn } from '../../../utils/bem'; import { cnSelect } from '../../SelectComponents/cnSelect'; import { cnSelectGroupLabel } from '../../SelectComponents/SelectGroupLabel/SelectGroupLabel'; @@ -11,10 +10,6 @@ import { defaultGetItemLabel, UserSelect, UserSelectProps } from '../UserSelect' import { cnUserSelectItem } from '../UserSelectItem/UserSelectItem'; import { cnUserSelectValue } from '../UserSelectValue/UserSelectValue'; -jest.mock('resize-observer-polyfill', () => { - return ResizeObserver; -}); - const animationDuration = 200; const testId = 'UserSelect'; const cnRenderValue = cn('RenderValue'); diff --git a/src/components/UserSelectDeprecated/__tests__/UserSelect.test.tsx b/src/components/UserSelectDeprecated/__tests__/UserSelect.test.tsx index dd0680c64..99340268e 100644 --- a/src/components/UserSelectDeprecated/__tests__/UserSelect.test.tsx +++ b/src/components/UserSelectDeprecated/__tests__/UserSelect.test.tsx @@ -2,16 +2,11 @@ import React, { useState } from 'react'; import { act, fireEvent, render, screen } from '@testing-library/react'; import { simpleItems } from '../__mocks__/data.mock'; -import ResizeObserver from '../../../../__mocks__/ResizeObserver'; import { getInitialsForName } from '../../Avatar/Avatar'; import { cnSelect } from '../../SelectComponentsDeprecated/cnSelect'; import { UserSelect } from '../UserSelect'; import { cnUserItem } from '../UserSelectItem/UserSelectItem'; -jest.mock('resize-observer-polyfill', () => { - return ResizeObserver; -}); - const testId = 'UserSelect'; const animationDuration = 200; diff --git a/src/hocs/withTooltip/__tests__/withTooltip.test.tsx b/src/hocs/withTooltip/__tests__/withTooltip.test.tsx index f84603a07..c668171a4 100644 --- a/src/hocs/withTooltip/__tests__/withTooltip.test.tsx +++ b/src/hocs/withTooltip/__tests__/withTooltip.test.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { act, fireEvent, render, screen } from '@testing-library/react'; -import ResizeObserver from '../../../../__mocks__/ResizeObserver'; import { Button } from '../../../components/Button/Button'; import { appearTimeoutDefault, @@ -10,10 +9,6 @@ import { withTooltip, } from '../withTooltip'; -jest.mock('resize-observer-polyfill', () => { - return ResizeObserver; -}); - const testId = 'withTooltip'; const tooltipRole = 'Tooltip'; diff --git a/src/hooks/useComponentSize/useComponentSize.tsx b/src/hooks/useComponentSize/useComponentSize.tsx index 5ec54157a..9d9d08c01 100644 --- a/src/hooks/useComponentSize/useComponentSize.tsx +++ b/src/hooks/useComponentSize/useComponentSize.tsx @@ -1,4 +1,6 @@ -import { useCallback, useLayoutEffect, useState } from 'react'; +import { useMemo } from 'react'; + +import { useResizeObserved } from '../useResizeObserved/useResizeObserved'; type ComponentSize = { width: number; @@ -18,34 +20,10 @@ const getElementSize = (el: HTMLElement | SVGGraphicsElement | null): ComponentS }; }; -export function useComponentSize( - ref: React.RefObject, +export function useComponentSize( + ref: React.RefObject, ): ComponentSize { - const [componentSize, setComponentSize] = useState( - getElementSize(ref && ref.current), - ); - - const handleResize = useCallback(() => { - if (ref.current) { - setComponentSize(getElementSize(ref.current)); - } - }, [ref]); - - useLayoutEffect(() => { - if (!ref.current) { - return; - } - - handleResize(); - - const resizeObserver = new ResizeObserver(handleResize); - - resizeObserver.observe(ref.current); - - return () => { - resizeObserver.disconnect(); - }; - }, [ref, handleResize]); - + const refs = useMemo(() => [ref], [ref]); + const [componentSize] = useResizeObserved(refs, getElementSize); return componentSize; } diff --git a/src/hooks/useResizeObserved/useResizeObserved.ts b/src/hooks/useResizeObserved/useResizeObserved.ts new file mode 100644 index 000000000..56480e681 --- /dev/null +++ b/src/hooks/useResizeObserved/useResizeObserved.ts @@ -0,0 +1,32 @@ +import React, { RefObject } from 'react'; + +export const useResizeObserved = ( + refs: Array>, + mapper: (el: ELEMENT | null) => RETURN_TYPE, +): RETURN_TYPE[] => { + const [dimensions, setDimensions] = React.useState(() => + refs.map((ref) => mapper(ref.current)), + ); + + // Храним маппер в рефке, чтобы если его передадут инлайн-функцией, это не вызвало бесконечные перерендеры + const mapperRef = React.useRef(mapper); + React.useLayoutEffect(() => { + mapperRef.current = mapper; + }, [mapper]); + + React.useLayoutEffect(() => { + const resizeObserver = new ResizeObserver(() => { + setDimensions(refs.map((ref) => mapperRef.current(ref.current))); + }); + + for (const ref of refs) { + ref.current && resizeObserver.observe(ref.current); + } + + return () => { + resizeObserver.disconnect(); + }; + }, [refs]); + + return dimensions; +};