diff --git a/CHANGELOG.md b/CHANGELOG.md index d2c20a371..5f8811c80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,51 @@ ### Removed -## [23.0.0] - 2023-10-30 - -### Removed - -- `Tooltip`: removed `horizontal` and `vertical` positions from the `tooltipPosition` options. Tooltips will still render to the opposite side in case there is not enough space on the chosen position. ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2796](https://github.com/teamleadercrm/ui/pull/2796)` +## [23.1.0] - 2023-11-06 + +### Changed + +- `WysiwygEditor`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `ValidationText`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `WarningText`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `SuccessText`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `HelpText`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `ErrorText`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `Monospaced`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `Marker`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `Toggle`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `ToastContainer`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `Toast`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `Tag`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `TabGroup`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `SplitButton`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `RadioButton`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `ProgressTracker`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `Pagination`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `OverviewPage`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `Message`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `MarketingHeading2`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `MarketingHeading1`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `MarketingMarker`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `MarketingLockBadge`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `MarketingButtonGroup`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `LoadingBar`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `LabelValuePair`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `Island`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `GridItem`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `EmptyState`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `DetailPage`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `DatePicker`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `DataGrid`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `Counter`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `Container`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `Banner`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `AdvancedCollapsible`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `MarketingStatusLabel`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `MenuItem`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `MarketingMenuItem`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `Label`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) +- `Bullet`: implementeed ref forwarding ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2803](https://github.com/teamleadercrm/ui/pull/2803) ## [23.0.1] - 2023-10-30 @@ -20,6 +60,12 @@ - `Tooltip`: added missing `data-teamleader-ui="tooltip"` attribute ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2799](https://github.com/teamleadercrm/ui/pull/2799)` +## [23.0.0] - 2023-10-30 + +### Removed + +- `Tooltip`: removed `horizontal` and `vertical` positions from the `tooltipPosition` options. Tooltips will still render to the opposite side in case there is not enough space on the chosen position. ([@lowiebenoot](https://github.com/lowiebenoot)) in [#2796](https://github.com/teamleadercrm/ui/pull/2796)` + ## [22.3.5] - 2023-10-18 ### Fixed diff --git a/package.json b/package.json index d4130c854..f15baebe6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@teamleader/ui", "description": "Teamleader UI library", - "version": "23.0.1", + "version": "23.1.0", "author": "Teamleader ", "bugs": { "url": "https://github.com/teamleadercrm/ui/issues" diff --git a/src/components/advancedCollapsible/AdvancedCollapsible.tsx b/src/components/advancedCollapsible/AdvancedCollapsible.tsx index 1a3d26ff4..ef52a6a2a 100644 --- a/src/components/advancedCollapsible/AdvancedCollapsible.tsx +++ b/src/components/advancedCollapsible/AdvancedCollapsible.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useState } from 'react'; +import React, { ReactNode, forwardRef, useState } from 'react'; import { TextBody, Heading3 } from '../typography'; import Icon from '../icon'; import Box, { pickBoxProps } from '../box'; @@ -26,46 +26,47 @@ export interface AdvancedCollapsibleProps extends Omit { onChange?: (collapsed: boolean, event: React.MouseEvent) => void; } -const AdvancedCollapsible: GenericComponent = ({ - children, - color = 'teal', - indent = true, - size = 'medium', - title, - defaultIsCollapsed = true, - onChange, - ...others -}) => { - const [collapsed, setCollapsed] = useState(defaultIsCollapsed); +const AdvancedCollapsible: GenericComponent = forwardRef< + HTMLDivElement, + AdvancedCollapsibleProps +>( + ( + { children, color = 'teal', indent = true, size = 'medium', title, defaultIsCollapsed = true, onChange, ...others }, + ref, + ) => { + const [collapsed, setCollapsed] = useState(defaultIsCollapsed); - const boxProps = pickBoxProps(others); - const TitleElement = size === 'large' ? Heading3 : TextBody; + const boxProps = pickBoxProps(others); + const TitleElement = size === 'large' ? Heading3 : TextBody; - const handleTitleClick = (event: React.MouseEvent) => { - if (onChange) { - onChange(!collapsed, event); - } + const handleTitleClick = (event: React.MouseEvent) => { + if (onChange) { + onChange(!collapsed, event); + } - setCollapsed(!collapsed); - }; + setCollapsed(!collapsed); + }; - return ( - - - - {collapsed ? : } - - - {size === 'medium' ? {title} : title} - - - {!collapsed && ( - - {children} + return ( + + + + {collapsed ? : } + + + {size === 'medium' ? {title} : title} + - )} - - ); -}; + {!collapsed && ( + + {children} + + )} + + ); + }, +); + +AdvancedCollapsible.displayName = 'AdvancedCollapsible'; export default AdvancedCollapsible; diff --git a/src/components/banner/Banner.tsx b/src/components/banner/Banner.tsx index fdb49c52f..79548bde0 100644 --- a/src/components/banner/Banner.tsx +++ b/src/components/banner/Banner.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { forwardRef } from 'react'; import cx from 'classnames'; import Box, { BoxProps, Padding } from '../box'; @@ -30,43 +30,42 @@ export interface BannerProps extends Omit { size?: Exclude<(typeof SIZES)[number], 'tiny' | 'smallest' | 'hero' | 'fullscreen'>; } -const Banner = ({ - children, - className, - color = 'white', - size = 'medium', - icon, - onClose, - fullWidth, - ...others -}: BannerProps) => { - const classNames = cx(className, theme[color], theme['banner'], { [theme['banner_full-width']]: fullWidth }); +const Banner = forwardRef( + ( + { children, className, color = 'white', size = 'medium', icon, onClose, fullWidth, ...others }: BannerProps, + ref, + ) => { + const classNames = cx(className, theme[color], theme['banner'], { [theme['banner_full-width']]: fullWidth }); - return ( - -
- {icon && {icon}} - - {children} - - {onClose && ( - } - color={color === 'white' ? 'neutral' : color} - onClick={onClose} - /> - )} -
-
- ); -}; + return ( + +
+ {icon && {icon}} + + {children} + + {onClose && ( + } + color={color === 'white' ? 'neutral' : color} + onClick={onClose} + /> + )} +
+
+ ); + }, +); + +Banner.displayName = 'Banner'; export default Banner; diff --git a/src/components/bullet/Bullet.tsx b/src/components/bullet/Bullet.tsx index c7107c7de..781b369f7 100644 --- a/src/components/bullet/Bullet.tsx +++ b/src/components/bullet/Bullet.tsx @@ -1,9 +1,10 @@ -import React, { PureComponent } from 'react'; +import React, { forwardRef } from 'react'; import Box from '../box'; import cx from 'classnames'; import theme from './theme.css'; import { BoxProps } from '../box/Box'; import { COLORS, SIZES, TINTS } from '../../constants'; +import { GenericComponent } from '../../@types/types'; export interface BulletProps extends Omit { /** A border color to give to the counter */ @@ -18,9 +19,8 @@ export interface BulletProps extends Omit { size?: Exclude<(typeof SIZES)[number], 'tiny' | 'fullscreen' | 'smallest' | 'hero'>; } -class Bullet extends PureComponent { - render() { - const { className, color = 'neutral', size = 'medium', borderColor, borderTint, ...others } = this.props; +const Bullet: GenericComponent = forwardRef( + ({ className, color = 'neutral', size = 'medium', borderColor, borderTint, ...others }, ref) => { const classNames = cx( theme['bullet'], theme[color], @@ -32,8 +32,10 @@ class Bullet extends PureComponent { className, ); - return ; - } -} + return ; + }, +); + +Bullet.displayName = 'Bullet'; export default Bullet; diff --git a/src/components/container/Container.tsx b/src/components/container/Container.tsx index 00e1b775b..0259da3e3 100644 --- a/src/components/container/Container.tsx +++ b/src/components/container/Container.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, forwardRef } from 'react'; import Box from '../box'; import cx from 'classnames'; import theme from './theme.css'; @@ -11,20 +11,24 @@ export interface ContainerProps extends Omit { fixed?: boolean; } -const Container: GenericComponent = ({ children, className, fixed, ...others }) => { - const classNames = cx( - theme['container'], - { - [theme['is-fixed']]: fixed, - }, - className, - ); +const Container: GenericComponent = forwardRef( + ({ children, className, fixed, ...others }, ref) => { + const classNames = cx( + theme['container'], + { + [theme['is-fixed']]: fixed, + }, + className, + ); - return ( - - {children} - - ); -}; + return ( + + {children} + + ); + }, +); + +Container.displayName = 'Container'; export default Container; diff --git a/src/components/counter/Counter.tsx b/src/components/counter/Counter.tsx index bb24b770d..46f508556 100644 --- a/src/components/counter/Counter.tsx +++ b/src/components/counter/Counter.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, forwardRef } from 'react'; import Box from '../box'; import cx from 'classnames'; import theme from './theme.css'; @@ -27,34 +27,31 @@ export interface CounterProps extends Omit { size: 'small' | 'medium'; } -const Counter: GenericComponent = ({ - children, - className, - color = 'neutral', - count, - maxCount, - size = 'medium', - borderColor, - borderTint, - ...others -}) => { - const classNames = cx( - uiUtilities['reset-font-smoothing'], - theme['counter'], - theme[color], - theme[size], - { - [theme[`border-${borderColor}-${borderTint}`]]: borderColor && borderTint, - [theme[`border-${borderColor}`]]: borderColor && !borderTint, - }, - className, - ); +const Counter: GenericComponent = forwardRef( + ( + { children, className, color = 'neutral', count, maxCount, size = 'medium', borderColor, borderTint, ...others }, + ref, + ) => { + const classNames = cx( + uiUtilities['reset-font-smoothing'], + theme['counter'], + theme[color], + theme[size], + { + [theme[`border-${borderColor}-${borderTint}`]]: borderColor && borderTint, + [theme[`border-${borderColor}`]]: borderColor && !borderTint, + }, + className, + ); - return ( - - {count > maxCount ? `${maxCount}+` : count} {children} - - ); -}; + return ( + + {count > maxCount ? `${maxCount}+` : count} {children} + + ); + }, +); + +Counter.displayName = 'Counter'; export default Counter; diff --git a/src/components/datagrid/Cell.tsx b/src/components/datagrid/Cell.tsx index 289743211..b8477993c 100644 --- a/src/components/datagrid/Cell.tsx +++ b/src/components/datagrid/Cell.tsx @@ -1,4 +1,4 @@ -import React, { MouseEventHandler, ReactNode } from 'react'; +import React, { MouseEventHandler, ReactNode, forwardRef } from 'react'; import Box from '../box'; import cx from 'classnames'; import theme from './theme.css'; @@ -29,38 +29,43 @@ export interface CellProps extends Omit { onClick?: MouseEventHandler; } -const Cell: GenericComponent = ({ - align = 'left', - backgroundColor, - border, - children, - className, - flex = 1, - preventOverflow = true, - soft = false, - strong = false, - ...others -}) => { - const classNames = cx( - uiUtilities['reset-font-smoothing'], - theme['cell'], - theme[`align-${align}`], - theme[`flex-${flex}`], - theme[`has-background-${backgroundColor}`], - theme[`has-border-${border}`], +const Cell: GenericComponent = forwardRef( + ( { - [theme['is-soft']]: soft, - [theme['is-strong']]: strong, + align = 'left', + backgroundColor, + border, + children, + className, + flex = 1, + preventOverflow = true, + soft = false, + strong = false, + ...others }, - className, - ); + ref, + ) => { + const classNames = cx( + uiUtilities['reset-font-smoothing'], + theme['cell'], + theme[`align-${align}`], + theme[`flex-${flex}`], + theme[`has-background-${backgroundColor}`], + theme[`has-border-${border}`], + { + [theme['is-soft']]: soft, + [theme['is-strong']]: strong, + }, + className, + ); - return ( - - {preventOverflow ?
{children}
: children} -
- ); -}; + return ( + + {preventOverflow ?
{children}
: children} +
+ ); + }, +); Cell.displayName = 'DataGrid.Cell'; export default Cell; diff --git a/src/components/datagrid/DataGrid.tsx b/src/components/datagrid/DataGrid.tsx index f2c064991..80e203d2e 100644 --- a/src/components/datagrid/DataGrid.tsx +++ b/src/components/datagrid/DataGrid.tsx @@ -1,6 +1,6 @@ import cx from 'classnames'; import omit from 'lodash.omit'; -import React, { ChangeEvent, ReactElement, ReactNode, useEffect, useMemo, useRef, useState } from 'react'; +import React, { ChangeEvent, ReactElement, ReactNode, forwardRef, useEffect, useMemo, useRef, useState } from 'react'; import ReactResizeDetector from 'react-resize-detector'; import { GenericComponent } from '../../@types/types'; import Box from '../box'; @@ -17,7 +17,7 @@ import HeaderRow, { HeaderRowProps } from './HeaderRow'; import HeaderRowOverlay, { HeaderRowOverlayProps } from './HeaderRowOverlay/HeaderRowOverlay'; import theme from './theme.css'; -export interface DataGridProps extends Omit { +export interface DataGridProps extends Omit { /** If true, datagrid will have a border and rounded corners. */ bordered?: boolean; /** The content to display inside the data grid. */ @@ -47,229 +47,237 @@ interface DatagridComponent extends GenericComponent { FooterRow: GenericComponent; } -export const DataGrid: DatagridComponent = ({ - bordered = false, - children, - className, - processing = false, - comparableId, - selectable, - stickyFromLeft = 0, - stickyFromRight = 0, - onSelectionChange, - ...others -}) => { - const [hoveredRow, setHoveredRow] = useState(null); - const [isOverflowing, setOverflowing] = useState(false); - const [selectedRows, setSelectedRows] = useState([]); - const scrollableNode = useRef(null); - const [rowNodes, setRowNodes] = useState>(new Map()); - const totalRowChildrenWidth = useMemo( - () => - Array.from(rowNodes.values()) - .filter((rowDOMNode) => rowDOMNode.children) - .map((rowDOMNode) => - Array.from(rowDOMNode.children) - .map((child) => child.clientWidth) - .reduce((accumulatedChildWidth, currentChildWidth) => accumulatedChildWidth + currentChildWidth, 0), - ) - .reduce((maxRowWidth, currentRowWidth) => (currentRowWidth > maxRowWidth ? currentRowWidth : maxRowWidth), 0), - // eslint-disable-next-line react-hooks/exhaustive-deps - [rowNodes, rowNodes.size], - ); - const childrenArray: (ReactElement | ReactElement[])[] = !Array.isArray(children) ? [children] : children; - const bodyRowCount = (childrenArray.find((child) => Array.isArray(child)) as ReactElement[] | undefined)?.length; - - const handleSelectionChange = (selection: React.Key[], event: ChangeEvent | null = null) => { - if (onSelectionChange) { - onSelectionChange(selection, event); - } - }; +export const DataGrid: GenericComponent = forwardRef( + ( + { + bordered = false, + children, + className, + processing = false, + comparableId, + selectable, + stickyFromLeft = 0, + stickyFromRight = 0, + onSelectionChange, + ...others + }, + ref, + ) => { + const [hoveredRow, setHoveredRow] = useState(null); + const [isOverflowing, setOverflowing] = useState(false); + const [selectedRows, setSelectedRows] = useState([]); + const scrollableNode = useRef(null); + const [rowNodes, setRowNodes] = useState>(new Map()); + const totalRowChildrenWidth = useMemo( + () => + Array.from(rowNodes.values()) + .filter((rowDOMNode) => rowDOMNode.children) + .map((rowDOMNode) => + Array.from(rowDOMNode.children) + .map((child) => child.clientWidth) + .reduce((accumulatedChildWidth, currentChildWidth) => accumulatedChildWidth + currentChildWidth, 0), + ) + .reduce((maxRowWidth, currentRowWidth) => (currentRowWidth > maxRowWidth ? currentRowWidth : maxRowWidth), 0), + // eslint-disable-next-line react-hooks/exhaustive-deps + [rowNodes, rowNodes.size], + ); + const childrenArray: (ReactElement | ReactElement[])[] = !Array.isArray(children) ? [children] : children; + const bodyRowCount = (childrenArray.find((child) => Array.isArray(child)) as ReactElement[] | undefined)?.length; - const handleHeaderRowSelectionChange = (checked: boolean, event: ChangeEvent) => { - const allBodyRowIndexes = React.Children.map(children, (child) => { - if (isReactElement(child) && isComponentOfType(BodyRow, child)) { - return child.key; + const handleSelectionChange = (selection: React.Key[], event: ChangeEvent | null = null) => { + if (onSelectionChange) { + onSelectionChange(selection, event); } - }); + }; - const selectedBodyRowIndexes = checked ? allBodyRowIndexes ?? [] : []; - setSelectedRows(selectedBodyRowIndexes); - handleSelectionChange(selectedBodyRowIndexes, event); - }; + const handleHeaderRowSelectionChange = (checked: boolean, event: ChangeEvent) => { + const allBodyRowIndexes = React.Children.map(children, (child) => { + if (isReactElement(child) && isComponentOfType(BodyRow, child)) { + return child.key; + } + }); - const handleBodyRowMouseEnter = (row: ReactElement, event: MouseEvent) => { - const { onClick, onMouseOver } = row.props; + const selectedBodyRowIndexes = checked ? allBodyRowIndexes ?? [] : []; + setSelectedRows(selectedBodyRowIndexes); + handleSelectionChange(selectedBodyRowIndexes, event); + }; - onClick && setHoveredRow(row.key); - onMouseOver && onMouseOver(event); - }; + const handleBodyRowMouseEnter = (row: ReactElement, event: MouseEvent) => { + const { onClick, onMouseOver } = row.props; - const handleBodyRowMouseLeave = (row: ReactElement, event: MouseEvent) => { - const { onClick, onMouseOut } = row.props; + onClick && setHoveredRow(row.key); + onMouseOver && onMouseOver(event); + }; - onClick && setHoveredRow(null); - onMouseOut && onMouseOut(event); - }; + const handleBodyRowMouseLeave = (row: ReactElement, event: MouseEvent) => { + const { onClick, onMouseOut } = row.props; - const handleBodyRowSelectionChange = (rowIndex: React.Key | null, event: ChangeEvent) => { - if (rowIndex === null) { - return; - } + onClick && setHoveredRow(null); + onMouseOut && onMouseOut(event); + }; - const rows = selectedRows.includes(rowIndex) - ? selectedRows.filter((row) => row !== rowIndex) - : [...selectedRows, rowIndex]; + const handleBodyRowSelectionChange = (rowIndex: React.Key | null, event: ChangeEvent) => { + if (rowIndex === null) { + return; + } - setSelectedRows(rows); - handleSelectionChange(rows, event); - }; + const rows = selectedRows.includes(rowIndex) + ? selectedRows.filter((row) => row !== rowIndex) + : [...selectedRows, rowIndex]; - const handleResize = () => { - if (isElementOverflowingX(scrollableNode.current)) { - setOverflowing(true); - } else { - setOverflowing(false); - } - }; + setSelectedRows(rows); + handleSelectionChange(rows, event); + }; - useEffect(() => { - handleResize(); - }); + const handleResize = () => { + if (isElementOverflowingX(scrollableNode.current)) { + setOverflowing(true); + } else { + setOverflowing(false); + } + }; - useEffect(() => { - setSelectedRows([]); - handleSelectionChange([]); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [comparableId]); + useEffect(() => { + handleResize(); + }); - const classNames = cx( - theme['data-grid'], - { - [theme['is-bordered']]: bordered, - [theme['is-overflowing']]: isOverflowing, - }, - className, - ); + useEffect(() => { + setSelectedRows([]); + handleSelectionChange([]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [comparableId]); - const rest = omit(others, ['comparableId', 'onSelectionChange']); + const classNames = cx( + theme['data-grid'], + { + [theme['is-bordered']]: bordered, + [theme['is-overflowing']]: isOverflowing, + }, + className, + ); - const sectionLeftClassNames = cx(theme['section'], theme['is-sticky-left'], { - [theme['has-blend-right']]: selectable || stickyFromLeft > 0, - [theme['has-border-right']]: selectable || stickyFromLeft > 0, - }); + const rest = omit(others, ['comparableId', 'onSelectionChange']); - return ( - - {processing && ( -
- -
- )} - {selectedRows.length > 0 && - React.Children.map(children, (child) => { - if (isReactElement(child) && isComponentOfType(HeaderRowOverlay, child)) { - return React.cloneElement(child, { - numSelectedRows: selectedRows.length, - }); - } - })} - - {(selectable || stickyFromLeft > 0) && ( -
- {React.Children.map(children, (child) => { - if (isReactElement(child)) { - if (isComponentOfType(HeaderRow, child)) { - return React.cloneElement(child, { - onSelectionChange: handleHeaderRowSelectionChange, - selected: selectedRows.length === bodyRowCount, - selectable, - sliceTo: stickyFromLeft > 0 ? stickyFromLeft : 0, - }); - } else if (isComponentOfType(BodyRow, child)) { - return React.cloneElement(child, { - hovered: hoveredRow === child.key, - onMouseEnter: (event: MouseEvent) => handleBodyRowMouseEnter(child, event), - onMouseLeave: (event: MouseEvent) => handleBodyRowMouseLeave(child, event), - onSelectionChange: (checked: boolean, event: ChangeEvent) => - handleBodyRowSelectionChange(child.key, event), - selected: child.key ? selectedRows.indexOf(child.key) !== -1 : false, - selectable, - sliceTo: stickyFromLeft > 0 ? stickyFromLeft : 0, - }); - } else if (isComponentOfType(FooterRow, child)) { - return React.cloneElement(child, { - preserveSelectableSpace: selectable, - sliceTo: stickyFromLeft > 0 ? stickyFromLeft : 0, - }); - } - } - })} + const sectionLeftClassNames = cx(theme['section'], theme['is-sticky-left'], { + [theme['has-blend-right']]: selectable || stickyFromLeft > 0, + [theme['has-border-right']]: selectable || stickyFromLeft > 0, + }); + + return ( + + {processing && ( +
+
)} -
- {React.Children.map(children, (child, key) => { - if (isReactElement(child)) { - if (isComponentOfType(HeaderRow, child) || isComponentOfType(FooterRow, child)) { - return React.cloneElement(child, { - sliceFrom: stickyFromLeft > 0 ? stickyFromLeft : 0, - sliceTo: stickyFromRight > 0 ? -stickyFromRight : undefined, - ref: (rowNode: HTMLElement | null) => rowNode && setRowNodes(rowNodes.set(key, rowNode)), - style: isOverflowing - ? { - minWidth: `${totalRowChildrenWidth - 10}px`, - } - : undefined, - }); - } else if (isComponentOfType(BodyRow, child)) { - return React.cloneElement(child, { - hovered: hoveredRow === child.key, - onMouseEnter: (event: MouseEvent) => handleBodyRowMouseEnter(child, event), - onMouseLeave: (event: MouseEvent) => handleBodyRowMouseLeave(child, event), - sliceFrom: stickyFromLeft > 0 ? stickyFromLeft : 0, - sliceTo: stickyFromRight > 0 ? -stickyFromRight : undefined, - ref: (rowNode: HTMLElement | null) => rowNode && setRowNodes(rowNodes.set(key, rowNode)), - style: isOverflowing - ? { - minWidth: `${totalRowChildrenWidth - 10}px`, - } - : undefined, - }); - } + {selectedRows.length > 0 && + React.Children.map(children, (child) => { + if (isReactElement(child) && isComponentOfType(HeaderRowOverlay, child)) { + return React.cloneElement(child, { + numSelectedRows: selectedRows.length, + }); } })} -
- {stickyFromRight > 0 && ( -
- {React.Children.map(children, (child) => { + + {(selectable || stickyFromLeft > 0) && ( +
+ {React.Children.map(children, (child) => { + if (isReactElement(child)) { + if (isComponentOfType(HeaderRow, child)) { + return React.cloneElement(child, { + onSelectionChange: handleHeaderRowSelectionChange, + selected: selectedRows.length === bodyRowCount, + selectable, + sliceTo: stickyFromLeft > 0 ? stickyFromLeft : 0, + }); + } else if (isComponentOfType(BodyRow, child)) { + return React.cloneElement(child, { + hovered: hoveredRow === child.key, + onMouseEnter: (event: MouseEvent) => handleBodyRowMouseEnter(child, event), + onMouseLeave: (event: MouseEvent) => handleBodyRowMouseLeave(child, event), + onSelectionChange: (checked: boolean, event: ChangeEvent) => + handleBodyRowSelectionChange(child.key, event), + selected: child.key ? selectedRows.indexOf(child.key) !== -1 : false, + selectable, + sliceTo: stickyFromLeft > 0 ? stickyFromLeft : 0, + }); + } else if (isComponentOfType(FooterRow, child)) { + return React.cloneElement(child, { + preserveSelectableSpace: selectable, + sliceTo: stickyFromLeft > 0 ? stickyFromLeft : 0, + }); + } + } + })} +
+ )} +
+ {React.Children.map(children, (child, key) => { if (isReactElement(child)) { if (isComponentOfType(HeaderRow, child) || isComponentOfType(FooterRow, child)) { - return React.cloneElement(child, { sliceFrom: -stickyFromRight }); + return React.cloneElement(child, { + sliceFrom: stickyFromLeft > 0 ? stickyFromLeft : 0, + sliceTo: stickyFromRight > 0 ? -stickyFromRight : undefined, + ref: (rowNode: HTMLElement | null) => rowNode && setRowNodes(rowNodes.set(key, rowNode)), + style: isOverflowing + ? { + minWidth: `${totalRowChildrenWidth - 10}px`, + } + : undefined, + }); } else if (isComponentOfType(BodyRow, child)) { return React.cloneElement(child, { hovered: hoveredRow === child.key, onMouseEnter: (event: MouseEvent) => handleBodyRowMouseEnter(child, event), onMouseLeave: (event: MouseEvent) => handleBodyRowMouseLeave(child, event), - sliceFrom: -stickyFromRight, + sliceFrom: stickyFromLeft > 0 ? stickyFromLeft : 0, + sliceTo: stickyFromRight > 0 ? -stickyFromRight : undefined, + ref: (rowNode: HTMLElement | null) => rowNode && setRowNodes(rowNodes.set(key, rowNode)), + style: isOverflowing + ? { + minWidth: `${totalRowChildrenWidth - 10}px`, + } + : undefined, }); } } })}
- )} + {stickyFromRight > 0 && ( +
+ {React.Children.map(children, (child) => { + if (isReactElement(child)) { + if (isComponentOfType(HeaderRow, child) || isComponentOfType(FooterRow, child)) { + return React.cloneElement(child, { sliceFrom: -stickyFromRight }); + } else if (isComponentOfType(BodyRow, child)) { + return React.cloneElement(child, { + hovered: hoveredRow === child.key, + onMouseEnter: (event: MouseEvent) => handleBodyRowMouseEnter(child, event), + onMouseLeave: (event: MouseEvent) => handleBodyRowMouseLeave(child, event), + sliceFrom: -stickyFromRight, + }); + } + } + })} +
+ )} +
+ - - - ); -}; - -DataGrid.HeaderRow = HeaderRow; -DataGrid.HeaderRowOverlay = HeaderRowOverlay; -DataGrid.HeaderCell = HeaderCell; -DataGrid.BodyRow = BodyRow; -DataGrid.Cell = Cell; -DataGrid.FooterRow = FooterRow; + ); + }, +); DataGrid.displayName = 'DataGrid'; -export default DataGrid; +// It has to be written like this, since `forwardRef` return component without sub-components and that doesn't match with our typing +const DataGridWithSubComponents = DataGrid as DatagridComponent; + +DataGridWithSubComponents.HeaderRow = HeaderRow; +DataGridWithSubComponents.HeaderRowOverlay = HeaderRowOverlay; +DataGridWithSubComponents.HeaderCell = HeaderCell; +DataGridWithSubComponents.BodyRow = BodyRow; +DataGridWithSubComponents.Cell = Cell; +DataGridWithSubComponents.FooterRow = FooterRow; + +export default DataGridWithSubComponents; diff --git a/src/components/datagrid/HeaderCell.tsx b/src/components/datagrid/HeaderCell.tsx index 2078b6979..334328df8 100644 --- a/src/components/datagrid/HeaderCell.tsx +++ b/src/components/datagrid/HeaderCell.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { forwardRef } from 'react'; import theme from './theme.css'; import Cell, { CellProps } from './Cell'; import Icon from '../icon'; @@ -14,46 +14,40 @@ export interface HeaderCellProps extends CellProps { sorted?: 'asc' | 'desc'; } -const HeaderCell: GenericComponent = ({ - align = 'left', - children, - className, - onClick, - sortable, - sorted, - ...others -}) => { - const renderSortedIndicators = () => { - if (sorted === 'asc' || (!sorted && sortable)) { - return ; - } +const HeaderCell: GenericComponent = forwardRef( + ({ align = 'left', children, className, onClick, sortable, sorted, ...others }, ref) => { + const renderSortedIndicators = () => { + if (sorted === 'asc' || (!sorted && sortable)) { + return ; + } - if (sorted === 'desc') { - return ; - } + if (sorted === 'desc') { + return ; + } - return null; - }; + return null; + }; - const classNames = cx( - theme['header-cell'], - { - [theme['is-sortable']]: sortable, - [theme['is-sorted']]: sorted === 'asc' || sorted === 'desc', - }, - className, - ); + const classNames = cx( + theme['header-cell'], + { + [theme['is-sortable']]: sortable, + [theme['is-sorted']]: sorted === 'asc' || sorted === 'desc', + }, + className, + ); - return ( - - {sortable && align === 'right' && {renderSortedIndicators()}} - - {children} - - {sortable && align === 'left' && {renderSortedIndicators()}} - - ); -}; + return ( + + {sortable && align === 'right' && {renderSortedIndicators()}} + + {children} + + {sortable && align === 'left' && {renderSortedIndicators()}} + + ); + }, +); HeaderCell.displayName = 'DataGrid.HeaderCell'; export default HeaderCell; diff --git a/src/components/datagrid/HeaderRowOverlay/HeaderRowOverlay.tsx b/src/components/datagrid/HeaderRowOverlay/HeaderRowOverlay.tsx index 0843fab33..c87ef80ec 100644 --- a/src/components/datagrid/HeaderRowOverlay/HeaderRowOverlay.tsx +++ b/src/components/datagrid/HeaderRowOverlay/HeaderRowOverlay.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, forwardRef } from 'react'; import cx from 'classnames'; import Box from '../../box'; import BulkActions from './BulkActions'; @@ -19,33 +19,29 @@ export interface HeaderRowOverlayProps { numSelectedRowsLabel?: (numSelectedRows?: number) => string; } -const HeaderRowOverlay: GenericComponent = ({ - children, - className, - headerCellCheckboxSize, - numSelectedRows = 0, - numSelectedRowsLabel, - ...others -}) => { - const classNames = cx( - theme['header-row-overlay'], - theme[`data-grid-checkbox-size-${headerCellCheckboxSize}`], - className, - ); +const HeaderRowOverlay: GenericComponent = forwardRef( + ({ children, className, headerCellCheckboxSize, numSelectedRows = 0, numSelectedRowsLabel, ...others }, ref) => { + const classNames = cx( + theme['header-row-overlay'], + theme[`data-grid-checkbox-size-${headerCellCheckboxSize}`], + className, + ); - return ( - - - - - ); -}; + return ( + + + + + ); + }, +); export default HeaderRowOverlay; HeaderRowOverlay.displayName = 'DataGrid.HeaderRowOverlay'; diff --git a/src/components/datepicker/DatePicker.tsx b/src/components/datepicker/DatePicker.tsx index 0c8825c62..13f0d39e4 100644 --- a/src/components/datepicker/DatePicker.tsx +++ b/src/components/datepicker/DatePicker.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, forwardRef } from 'react'; import DayPicker, { DayModifiers, DayPickerProps } from 'react-day-picker'; import Box, { pickBoxProps } from '../box'; import NavigationBar from './NavigationBar'; @@ -37,95 +37,102 @@ export interface DatePickerProps onDayClick?: (day: Date, modifiers: DayModifiers, event: React.MouseEvent) => void; } -const DatePicker: GenericComponent = ({ - bordered = true, - className, - modifiers, - size = 'medium', - withMonthPicker, - showWeekNumbers, - initialMonth, - onChange, - onDayClick, - ...others -}) => { - const [selectedDate, setSelectedDate] = useState(others.selectedDate); - const [selectedMonth, setSelectedMonth] = useState(); +const DatePicker: GenericComponent = forwardRef( + ( + { + bordered = true, + className, + modifiers, + size = 'medium', + withMonthPicker, + showWeekNumbers, + initialMonth, + onChange, + onDayClick, + ...others + }, + ref, + ) => { + const [selectedDate, setSelectedDate] = useState(others.selectedDate); + const [selectedMonth, setSelectedMonth] = useState(); - useEffect(() => { - setSelectedDate(others.selectedDate); - setSelectedMonth(others.selectedDate); - }, [others.selectedDate]); + useEffect(() => { + setSelectedDate(others.selectedDate); + setSelectedMonth(others.selectedDate); + }, [others.selectedDate]); - const handleDayClick = (day: Date, modifiers: DayModifiers, event: React.MouseEvent) => { - if (modifiers[theme['disabled']]) { - return; - } + const handleDayClick = (day: Date, modifiers: DayModifiers, event: React.MouseEvent) => { + if (modifiers[theme['disabled']]) { + return; + } - setSelectedDate(day); - onChange && onChange(day); - onDayClick && onDayClick(day, modifiers, event); - }; + setSelectedDate(day); + onChange && onChange(day); + onDayClick && onDayClick(day, modifiers, event); + }; - const handleYearMonthChange = (selectedMonth: Date) => { - setSelectedMonth(selectedMonth); - }; + const handleYearMonthChange = (selectedMonth: Date) => { + setSelectedMonth(selectedMonth); + }; - const getMonthPickerSize = () => { - const monthPickerSizeByDatePickerSize: Record< - string, - Exclude<(typeof SIZES)[number], 'tiny' | 'fullscreen' | 'hero'> - > = { - small: 'smallest', - medium: showWeekNumbers ? 'medium' : 'small', - large: 'large', + const getMonthPickerSize = () => { + const monthPickerSizeByDatePickerSize: Record< + string, + Exclude<(typeof SIZES)[number], 'tiny' | 'fullscreen' | 'hero'> + > = { + small: 'smallest', + medium: showWeekNumbers ? 'medium' : 'small', + large: 'large', + }; + + return monthPickerSizeByDatePickerSize[size]; }; - return monthPickerSizeByDatePickerSize[size]; - }; + const classNames = cx( + uiUtilities['reset-font-smoothing'], + theme['date-picker'], + theme[`is-${size}`], + { + [theme['is-bordered']]: bordered, + }, + className, + ); - const classNames = cx( - uiUtilities['reset-font-smoothing'], - theme['date-picker'], - theme[`is-${size}`], - { - [theme['is-bordered']]: bordered, - }, - className, - ); + return ( + + } + onDayClick={handleDayClick} + selectedDays={selectedDate} + weekdayElement={({ ...props }) => } + showWeekNumbers={showWeekNumbers} + fixedWeeks + captionElement={ + withMonthPicker + ? ({ date, locale, localeUtils }) => ( + + ) + : undefined + } + /> + + ); + }, +); - return ( - - } - onDayClick={handleDayClick} - selectedDays={selectedDate} - weekdayElement={({ ...props }) => } - showWeekNumbers={showWeekNumbers} - fixedWeeks - captionElement={ - withMonthPicker - ? ({ date, locale, localeUtils }) => ( - - ) - : undefined - } - /> - - ); -}; +DatePicker.displayName = 'DatePicker'; export default DatePicker; diff --git a/src/components/detailPage/DetailPage.tsx b/src/components/detailPage/DetailPage.tsx index ee381d9ff..73c643a5a 100644 --- a/src/components/detailPage/DetailPage.tsx +++ b/src/components/detailPage/DetailPage.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, forwardRef } from 'react'; import DetailPageBody, { DetailPageBodyProps } from './DetailPageBody'; import DetailPageHeader, { DetailPageHeaderProps } from './DetailPageHeader'; import Box from '../box'; @@ -14,15 +14,22 @@ interface DetailPageComponent extends GenericComponent { Header: GenericComponent; } -const DetailPage: DetailPageComponent = ({ children, ...others }) => { - return ( - - {children} - - ); -}; +const DetailPage: GenericComponent = forwardRef( + ({ children, ...others }, ref) => { + return ( + + {children} + + ); + }, +); -DetailPage.Body = DetailPageBody; -DetailPage.Header = DetailPageHeader; +DetailPage.displayName = 'DetailPage'; -export default DetailPage; +// It has to be written like this, since `forwardRef` return component without sub-components and that doesn't match with our typing +const DetailPageWithSubComponents = DetailPage as DetailPageComponent; + +DetailPageWithSubComponents.Body = DetailPageBody; +DetailPageWithSubComponents.Header = DetailPageHeader; + +export default DetailPageWithSubComponents; diff --git a/src/components/detailPage/DetailPageBody.tsx b/src/components/detailPage/DetailPageBody.tsx index 63d254cbc..708282cc2 100644 --- a/src/components/detailPage/DetailPageBody.tsx +++ b/src/components/detailPage/DetailPageBody.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, forwardRef } from 'react'; import { GenericComponent } from '../../@types/types'; import Container from '../container'; import { ContainerProps } from '../container/Container'; @@ -7,13 +7,15 @@ export interface DetailPageBodyProps extends Omit = ({ children, ...others }) => { - return ( - - {children} - - ); -}; +const DetailPageBody: GenericComponent = forwardRef( + ({ children, ...others }, ref) => { + return ( + + {children} + + ); + }, +); DetailPageBody.displayName = 'DetailPage.Body'; diff --git a/src/components/detailPage/DetailPageHeader.tsx b/src/components/detailPage/DetailPageHeader.tsx index 42f9ffb9d..7cdc2d810 100644 --- a/src/components/detailPage/DetailPageHeader.tsx +++ b/src/components/detailPage/DetailPageHeader.tsx @@ -1,5 +1,5 @@ import { IconArrowLeftSmallOutline } from '@teamleader/ui-icons'; -import React, { ReactNode } from 'react'; +import React, { ReactNode, forwardRef } from 'react'; import { GenericComponent } from '../../@types/types'; import BadgedLink from '../badgedLink'; import { BadgedLinkProps } from '../badgedLink/BadgedLink'; @@ -18,52 +18,48 @@ export interface DetailPageHeaderProps extends Omit { titleSuffix?: React.ReactNode; } -const DetailPageHeader: GenericComponent = ({ - backLinkProps, - children, - title, - titleColor = 'teal', - titleSuffix, - ...others -}) => { - return ( - - - {backLinkProps && ( - - } inherit={false} /> - - )} - - - - {title} - - {titleSuffix && {titleSuffix}} +const DetailPageHeader: GenericComponent = forwardRef( + ({ backLinkProps, children, title, titleColor = 'teal', titleSuffix, ...others }, ref) => { + return ( + + + {backLinkProps && ( + + } inherit={false} /> + + )} + + + + {title} + + {titleSuffix && {titleSuffix}} + + {children && ( + + {children} + + )} - {children && ( - - {children} - - )} - - - ); -}; + + ); + }, +); DetailPageHeader.displayName = 'DetailPage.Header'; diff --git a/src/components/emptyState/EmptyState.tsx b/src/components/emptyState/EmptyState.tsx index f4afaadd5..277484507 100644 --- a/src/components/emptyState/EmptyState.tsx +++ b/src/components/emptyState/EmptyState.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, forwardRef } from 'react'; import Box from '../box'; import cx from 'classnames'; import { @@ -28,51 +28,48 @@ const illustrationMap = { large: , }; -const EmptyState: GenericComponent = ({ - className, - metaText, - hidePointer = false, - size = 'medium', - title, - action, - ...others -}) => { - const classNames = cx( - theme['wrapper'], - theme[`is-${size}`], - { - [theme['has-pointer']]: title, - }, - className as string, - ); +const EmptyState: GenericComponent = forwardRef( + ({ className, metaText, hidePointer = false, size = 'medium', title, action, ...others }, ref) => { + const classNames = cx( + theme['wrapper'], + theme[`is-${size}`], + { + [theme['has-pointer']]: title, + }, + className as string, + ); - return ( - - {title && !hidePointer &&
{illustrationMap[size]}
} -
- {title && {title}} - {metaText && ( - - {metaText} - - )} - {action && ( - - } inherit={false} /> - - )} -
-
- ); -}; + return ( + + {title && !hidePointer &&
{illustrationMap[size]}
} +
+ {title && {title}} + {metaText && ( + + {metaText} + + )} + {action && ( + + } inherit={false} /> + + )} +
+
+ ); + }, +); + +EmptyState.displayName = 'EmptyState'; export default EmptyState; diff --git a/src/components/grid/GridItem.tsx b/src/components/grid/GridItem.tsx index 65f4fc4a9..400e334df 100644 --- a/src/components/grid/GridItem.tsx +++ b/src/components/grid/GridItem.tsx @@ -1,12 +1,20 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, forwardRef } from 'react'; import { GenericComponent } from '../../@types/types'; export type GridItemProps = Partial<{ children: ReactNode; area: string }>; -const GridItem: GenericComponent = ({ children, area }) => { - const gridItemStyles = { gridArea: area }; +const GridItem: GenericComponent = forwardRef( + ({ children, area }, ref) => { + const gridItemStyles = { gridArea: area }; - return
{children}
; -}; + return ( +
+ {children} +
+ ); + }, +); + +GridItem.displayName = 'Grid'; export default GridItem; diff --git a/src/components/island/Island.tsx b/src/components/island/Island.tsx index bb323eb38..e9446e6cd 100644 --- a/src/components/island/Island.tsx +++ b/src/components/island/Island.tsx @@ -1,4 +1,4 @@ -import React, { MouseEvent, ReactNode } from 'react'; +import React, { MouseEvent, ReactNode, forwardRef } from 'react'; import Box, { pickBoxProps } from '../box'; import cx from 'classnames'; import theme from './theme.css'; @@ -25,37 +25,35 @@ export interface IslandProps extends Omit { size?: Exclude<(typeof SIZES)[number], 'tiny' | 'smallest' | 'hero' | 'fullscreen'>; } -const Island: GenericComponent = ({ - children, - className, - color = 'white', - size = 'medium', - onClick, - ...others -}: IslandProps) => { - const classNames = cx(theme[color], className); - const boxProps = pickBoxProps(others); +const Island: GenericComponent = forwardRef( + ({ children, className, color = 'white', size = 'medium', onClick, ...others }: IslandProps, ref) => { + const classNames = cx(theme[color], className); + const boxProps = pickBoxProps(others); - return ( - - {children} - - ); -}; + return ( + + {children} + + ); + }, +); + +Island.displayName = 'Island'; export default Island; diff --git a/src/components/label/Label.tsx b/src/components/label/Label.tsx index 5fb3594be..ce92d1015 100644 --- a/src/components/label/Label.tsx +++ b/src/components/label/Label.tsx @@ -1,5 +1,5 @@ import { IconInfoBadgedSmallFilled } from '@teamleader/ui-icons'; -import React, { ReactNode } from 'react'; +import React, { ReactNode, forwardRef } from 'react'; import { GenericComponent } from '../../@types/types'; import { SIZES } from '../../constants'; import Box from '../box'; @@ -19,59 +19,55 @@ export interface LabelProps extends Omit { tooltipProps?: Record; } -const Label: GenericComponent = ({ - children, - inverse = false, - required = false, - size = 'medium', - tooltip, - tooltipProps, - ...others -}) => { - const childProps = { - inverse, - marginTop: 1, - size, - }; +const Label: GenericComponent = forwardRef( + ({ children, inverse = false, required = false, size = 'medium', tooltip, tooltipProps, ...others }, ref) => { + const childProps = { + inverse, + marginTop: 1, + size, + }; - const Element = { - small: TextBodyCompact, - medium: TextBodyCompact, - large: TextDisplay, - }[size]; + const Element = { + small: TextBodyCompact, + medium: TextBodyCompact, + large: TextDisplay, + }[size]; - return ( - - {React.Children.map(children, (child) => - typeof child !== 'string' && React.isValidElement(child) ? ( - React.cloneElement(child, { ...childProps, ...child.props }) - ) : ( - - - {child} - - {required && ( - - * - - )} - {tooltip && ( - {tooltip}} - tooltipSize="small" - color={inverse ? 'neutral' : 'teal'} - tint={inverse ? 'lightest' : 'darkest'} - marginLeft={1} - {...tooltipProps} - > - - - )} - - ), - )} - - ); -}; + return ( + + {React.Children.map(children, (child) => + typeof child !== 'string' && React.isValidElement(child) ? ( + React.cloneElement(child, { ...childProps, ...child.props }) + ) : ( + + + {child} + + {required && ( + + * + + )} + {tooltip && ( + {tooltip}} + tooltipSize="small" + color={inverse ? 'neutral' : 'teal'} + tint={inverse ? 'lightest' : 'darkest'} + marginLeft={1} + {...tooltipProps} + > + + + )} + + ), + )} + + ); + }, +); + +Label.displayName = 'Label'; export default Label; diff --git a/src/components/labelValuePair/Label.tsx b/src/components/labelValuePair/Label.tsx index a356edec5..1c54dcdd8 100644 --- a/src/components/labelValuePair/Label.tsx +++ b/src/components/labelValuePair/Label.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, forwardRef } from 'react'; import { GenericComponent } from '../../@types/types'; import { BoxProps } from '../box/Box'; import { Heading5 } from '../typography'; @@ -8,18 +8,23 @@ export interface LabelProps extends Omit { children?: ReactNode; } -const Label: GenericComponent = ({ children, inline, ...others }) => ( - - {children} - +const Label: GenericComponent = forwardRef( + ({ children, inline, ...others }, ref) => ( + + {children} + + ), ); +Label.displayName = 'LabelValuePair.Label'; + export default Label; diff --git a/src/components/labelValuePair/LabelValuePair.tsx b/src/components/labelValuePair/LabelValuePair.tsx index a952ff1ef..0024b2c9d 100644 --- a/src/components/labelValuePair/LabelValuePair.tsx +++ b/src/components/labelValuePair/LabelValuePair.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, forwardRef } from 'react'; import { GenericComponent } from '../../@types/types'; import Box from '../box'; import { BoxProps } from '../box/Box'; @@ -7,7 +7,7 @@ import isReactElement from '../utils/is-react-element'; import Label, { LabelProps } from './Label'; import Value, { ValueProps } from './Value'; -export interface LabelValuePairProps extends Omit { +export interface LabelValuePairProps extends Omit { alignValue?: 'left' | 'right'; children: ReactNode; inline?: boolean; @@ -18,38 +18,44 @@ interface LabelValuePairComponent extends GenericComponent Value: GenericComponent; } -const LabelValuePair: LabelValuePairComponent = ({ alignValue = 'left', children, inline = true, ...others }) => ( - - {React.Children.map(children, (child) => { - if (!isReactElement(child)) { - return null; - } - if (isComponentOfType(Label, child) && React.isValidElement(child)) { - return React.cloneElement(child, { inline, ...child.props }); - } - - if (isComponentOfType(Value, child) && React.isValidElement(child)) { - return React.cloneElement(child, { - justifyContent: alignValue === 'left' ? 'flex-start' : 'flex-end', - paddingVertical: inline ? 1 : 0, - textAlign: alignValue, - // @ts-ignore TS acting weird, child.props is there - ...child.props, - }); - } - })} - +const LabelValuePair: GenericComponent = forwardRef( + ({ alignValue = 'left', children, inline = true, ...others }, ref) => ( + + {React.Children.map(children, (child) => { + if (!isReactElement(child)) { + return null; + } + if (isComponentOfType(Label, child) && React.isValidElement(child)) { + return React.cloneElement(child, { inline, ...child.props }); + } + + if (isComponentOfType(Value, child) && React.isValidElement(child)) { + return React.cloneElement(child, { + justifyContent: alignValue === 'left' ? 'flex-start' : 'flex-end', + paddingVertical: inline ? 1 : 0, + textAlign: alignValue, + // @ts-ignore TS acting weird, child.props is there + ...child.props, + }); + } + })} + + ), ); -LabelValuePair.Label = Label; -LabelValuePair.Label.displayName = 'LabelValuePair.Label'; -LabelValuePair.Value = Value; -LabelValuePair.Value.displayName = 'LabelValuePair.Value'; +LabelValuePair.displayName = 'LabelValuePair'; + +// It has to be written like this, since `forwardRef` return component without sub-components and that doesn't match with our typing +const LabelValuePairWithSubComponents = LabelValuePair as LabelValuePairComponent; + +LabelValuePairWithSubComponents.Label = Label; +LabelValuePairWithSubComponents.Value = Value; -export default LabelValuePair; +export default LabelValuePairWithSubComponents; diff --git a/src/components/labelValuePair/LabelValuePairGroup.tsx b/src/components/labelValuePair/LabelValuePairGroup.tsx index 81ab1ac7f..4b3c3bc10 100644 --- a/src/components/labelValuePair/LabelValuePairGroup.tsx +++ b/src/components/labelValuePair/LabelValuePairGroup.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, forwardRef } from 'react'; import { GenericComponent } from '../../@types/types'; import Box from '../box'; import { BoxProps } from '../box/Box'; @@ -9,15 +9,20 @@ export interface LabelValuePairGroupProps extends Omit { title?: string | ReactNode; } -const LabelValuePairGroup: GenericComponent = ({ children, title, ...others }) => { +const LabelValuePairGroup: GenericComponent = forwardRef< + HTMLElement, + LabelValuePairGroupProps +>(({ children, title, ...others }, ref) => { return ( - + {title} {children} ); -}; +}); + +LabelValuePairGroup.displayName = 'LabelValuePairGroup'; export default LabelValuePairGroup; diff --git a/src/components/labelValuePair/Value.tsx b/src/components/labelValuePair/Value.tsx index f89d5d38c..7fea33aad 100644 --- a/src/components/labelValuePair/Value.tsx +++ b/src/components/labelValuePair/Value.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, forwardRef } from 'react'; import { GenericComponent } from '../../@types/types'; import Box from '../box'; import { BoxProps } from '../box/Box'; @@ -7,10 +7,12 @@ export interface ValueProps extends Omit { children?: ReactNode; } -const Value: GenericComponent = ({ children, ...others }) => ( - +const Value: GenericComponent = forwardRef(({ children, ...others }, ref) => ( + {children} -); +)); + +Value.displayName = 'LabelValuePair.Value'; export default Value; diff --git a/src/components/loadingBar/LoadingBar.tsx b/src/components/loadingBar/LoadingBar.tsx index a33afdffd..1ac6ab752 100644 --- a/src/components/loadingBar/LoadingBar.tsx +++ b/src/components/loadingBar/LoadingBar.tsx @@ -1,5 +1,5 @@ import cx from 'classnames'; -import React from 'react'; +import React, { forwardRef } from 'react'; import { GenericComponent } from '../../@types/types'; import { COLORS, SIZES, TINTS } from '../../constants'; import Box from '../box'; @@ -17,25 +17,23 @@ export interface LoadingBarProps extends Omit { tint?: (typeof TINTS)[number]; } -const LoadingBar: GenericComponent = ({ - className, - color = 'mint', - size = 'small', - tint = 'normal', - ...others -}) => { - const classNames = cx( - theme['loading-bar'], - theme[`is-${color}`], - theme[`is-${size}`], - theme[`is-${tint}`], - className, - ); - return ( - -
- - ); -}; +const LoadingBar: GenericComponent = forwardRef( + ({ className, color = 'mint', size = 'small', tint = 'normal', ...others }, ref) => { + const classNames = cx( + theme['loading-bar'], + theme[`is-${color}`], + theme[`is-${size}`], + theme[`is-${tint}`], + className, + ); + return ( + +
+ + ); + }, +); + +LoadingBar.displayName = 'LoadingBar'; export default LoadingBar; diff --git a/src/components/marketingButtonGroup/MarketingButtonGroup.tsx b/src/components/marketingButtonGroup/MarketingButtonGroup.tsx index c03949db0..7aedb1259 100644 --- a/src/components/marketingButtonGroup/MarketingButtonGroup.tsx +++ b/src/components/marketingButtonGroup/MarketingButtonGroup.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, forwardRef } from 'react'; import omit from 'lodash.omit'; import Box, { omitBoxProps } from '../box'; import Button, { ButtonProps } from './Button'; @@ -23,7 +23,10 @@ interface MarketingButtonGroupComponent extends GenericComponent; } -const MarketingButtonGroup: MarketingButtonGroupComponent = ({ children, className, value, onChange, ...others }) => { +const MarketingButtonGroup: GenericComponent = forwardRef< + HTMLElement, + MarketingButtonGroupProps +>(({ children, className, value, onChange, ...others }, ref) => { const handleChange = (value: string, event: React.ChangeEvent) => { if (onChange) { onChange(value, event); @@ -33,7 +36,7 @@ const MarketingButtonGroup: MarketingButtonGroupComponent = ({ children, classNa const classNames = cx(theme['group'], theme['segmented'], className); return ( - + {React.Children.map(children, (child) => { if (!React.isValidElement(child)) { return; @@ -62,8 +65,13 @@ const MarketingButtonGroup: MarketingButtonGroupComponent = ({ children, classNa })} ); -}; +}); -MarketingButtonGroup.Button = Button; +MarketingButtonGroup.displayName = 'MarketingButtonGroup'; -export default MarketingButtonGroup; +// It has to be written like this, since `forwardRef` return component without sub-components and that doesn't match with our typing +const MarketingButtonGroupWithSubComponents = MarketingButtonGroup as MarketingButtonGroupComponent; + +MarketingButtonGroupWithSubComponents.Button = Button; + +export default MarketingButtonGroupWithSubComponents; diff --git a/src/components/marketingLockBadge/MarketingLockBadge.tsx b/src/components/marketingLockBadge/MarketingLockBadge.tsx index 9de86e910..bedef8848 100644 --- a/src/components/marketingLockBadge/MarketingLockBadge.tsx +++ b/src/components/marketingLockBadge/MarketingLockBadge.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { forwardRef } from 'react'; import Box from '../box'; import Icon from '../icon'; import cx from 'classnames'; @@ -13,24 +13,29 @@ export interface MarketingLockBadgeProps extends Omit { size?: Exclude<(typeof SIZES)[number], 'tiny' | 'large' | 'fullscreen' | 'smallest' | 'hero'>; } -const MarketingLockBadge: GenericComponent = ({ className, size = 'medium', ...others }) => { - const classNames = cx(theme['wrapper'], theme[`is-${size}`], className); +const MarketingLockBadge: GenericComponent = forwardRef( + ({ className, size = 'medium', ...others }, ref) => { + const classNames = cx(theme['wrapper'], theme[`is-${size}`], className); - return ( - - - - - - ); -}; + return ( + + + + + + ); + }, +); + +MarketingLockBadge.displayName = 'MarketingLockBadge'; export default MarketingLockBadge; diff --git a/src/components/marketingMarker/MarketingMarker.tsx b/src/components/marketingMarker/MarketingMarker.tsx index 761ba5279..c4d371aab 100644 --- a/src/components/marketingMarker/MarketingMarker.tsx +++ b/src/components/marketingMarker/MarketingMarker.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, forwardRef } from 'react'; import Box from '../box'; import cx from 'classnames'; import theme from './theme.css'; @@ -10,21 +10,26 @@ export interface MarketingMarkerProps extends Omit { className?: string; } -const MarketingMarker: GenericComponent = ({ children, className, ...others }) => { - const classNames = cx(theme['marker'], className); +const MarketingMarker: GenericComponent = forwardRef( + ({ children, className, ...others }, ref) => { + const classNames = cx(theme['marker'], className); - return ( - - {children} - - ); -}; + return ( + + {children} + + ); + }, +); + +MarketingMarker.displayName = 'MarketingMarker'; export default MarketingMarker; diff --git a/src/components/marketingMenuItem/MarketingMenuItem.tsx b/src/components/marketingMenuItem/MarketingMenuItem.tsx index f7cc1375e..ccb09b7c0 100644 --- a/src/components/marketingMenuItem/MarketingMenuItem.tsx +++ b/src/components/marketingMenuItem/MarketingMenuItem.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, MouseEvent } from 'react'; +import React, { ReactNode, MouseEvent, forwardRef } from 'react'; import Box, { omitBoxProps, pickBoxProps } from '../box'; import Icon from '../icon'; import { TextBodyCompact } from '../typography'; @@ -25,61 +25,56 @@ export interface MarketingMenuItemProps extends Omit = ({ - onClick, - icon, - caption, - className = '', - element = 'button', - label, - selected = false, - ...others -}) => { - const classNames = cx( - theme['marketing-menu-item'], - { - [theme['is-selected']]: selected, - }, - className, - ); +const MarketingMenuItem: GenericComponent = forwardRef( + ({ onClick, icon, caption, className = '', element = 'button', label, selected = false, ...others }, ref) => { + const classNames = cx( + theme['marketing-menu-item'], + { + [theme['is-selected']]: selected, + }, + className, + ); - const boxProps = pickBoxProps(others); - const restProps = omitBoxProps(others); + const boxProps = pickBoxProps(others); + const restProps = omitBoxProps(others); - return ( - - - + return ( + - {label} - {caption && ( - - {caption} - - )} + + + {label} + {caption && ( + + {caption} + + )} + + {icon && {icon}} - {icon && {icon}} - - ); -}; + ); + }, +); + +MarketingMenuItem.displayName = 'MarketingMenuItem'; export default MarketingMenuItem; diff --git a/src/components/marketingStatusLabel/MarketingStatusLabel.tsx b/src/components/marketingStatusLabel/MarketingStatusLabel.tsx index 253aa1ed1..f5d2a319e 100644 --- a/src/components/marketingStatusLabel/MarketingStatusLabel.tsx +++ b/src/components/marketingStatusLabel/MarketingStatusLabel.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, forwardRef } from 'react'; import Box from '../box'; import Icon from '../icon'; import cx from 'classnames'; @@ -16,14 +16,10 @@ export interface MarketingStatusLabelProps extends Omit = ({ - children, - className, - fullWidth = false, - size = 'medium', - icon, - ...others -}) => { +const MarketingStatusLabel: GenericComponent = forwardRef< + HTMLElement, + MarketingStatusLabelProps +>(({ children, className, fullWidth = false, size = 'medium', icon, ...others }, ref) => { const classNames = cx(theme['wrapper'], theme[`is-${size}`], className); const TextElement = size === 'small' ? UITextSmall : UITextBody; @@ -37,6 +33,7 @@ const MarketingStatusLabel: GenericComponent = ({ justifyContent="center" className={classNames} paddingHorizontal={2} + ref={ref} > {children} {icon && ( @@ -46,6 +43,8 @@ const MarketingStatusLabel: GenericComponent = ({ )} ); -}; +}); + +MarketingStatusLabel.displayName = 'MarketingStatusLabel'; export default MarketingStatusLabel; diff --git a/src/components/marketingTypography/MarketingHeading1.tsx b/src/components/marketingTypography/MarketingHeading1.tsx index aa193a2eb..5fe69fd56 100644 --- a/src/components/marketingTypography/MarketingHeading1.tsx +++ b/src/components/marketingTypography/MarketingHeading1.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, forwardRef } from 'react'; import Box from '../box'; import cx from 'classnames'; import theme from './theme.css'; @@ -10,14 +10,19 @@ export interface MarketingHeadingProps extends Omit { className?: string; } -const MarketingHeading1: GenericComponent = ({ children, className, ...others }) => { +const MarketingHeading1: GenericComponent = forwardRef< + HTMLHeadingElement, + MarketingHeadingProps +>(({ children, className, ...others }, ref) => { const classNames = cx(theme['heading-1'], className); return ( - + {children} ); -}; +}); + +MarketingHeading1.displayName = 'MarketingHeading1'; export default MarketingHeading1; diff --git a/src/components/marketingTypography/MarketingHeading2.tsx b/src/components/marketingTypography/MarketingHeading2.tsx index 9e9e67d80..eade5acde 100644 --- a/src/components/marketingTypography/MarketingHeading2.tsx +++ b/src/components/marketingTypography/MarketingHeading2.tsx @@ -1,18 +1,23 @@ -import React from 'react'; +import React, { forwardRef } from 'react'; import Box from '../box'; import cx from 'classnames'; import theme from './theme.css'; import { GenericComponent } from '../../@types/types'; import { MarketingHeadingProps } from './MarketingHeading1'; -const MarketingHeading2: GenericComponent = ({ children, className, ...others }) => { +const MarketingHeading2: GenericComponent = forwardRef< + HTMLHeadingElement, + MarketingHeadingProps +>(({ children, className, ...others }, ref) => { const classNames = cx(theme['heading-2'], className); return ( - + {children} ); -}; +}); + +MarketingHeading2.displayName = 'MarketingHeading2'; export default MarketingHeading2; diff --git a/src/components/menu/MenuItem.tsx b/src/components/menu/MenuItem.tsx index e88787d62..c8117465c 100644 --- a/src/components/menu/MenuItem.tsx +++ b/src/components/menu/MenuItem.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, MouseEvent, CSSProperties } from 'react'; +import React, { ReactNode, MouseEvent, CSSProperties, forwardRef } from 'react'; import Box, { omitBoxProps, pickBoxProps } from '../box'; import Icon from '../icon'; import { TextBody } from '../typography'; @@ -32,84 +32,91 @@ export interface MenuItemProps extends Omit = ({ - onClick, - disabled = false, - icon, - caption, - children, - className = '', - style, - destructive = false, - element = 'button', - label, - selected = false, - ...others -}) => { - const handleClick = (event: MouseEvent) => { - if (onClick && !disabled) { - onClick(event); - } - }; - - const classNames = cx( - theme['menu-item'], +const MenuItem: GenericComponent = forwardRef( + ( { - [theme['is-selected']]: selected, - [theme['is-disabled']]: disabled, + onClick, + disabled = false, + icon, + caption, + children, + className = '', + style, + destructive = false, + element = 'button', + label, + selected = false, + ...others }, - className, - ); + ref, + ) => { + const handleClick = (event: MouseEvent) => { + if (onClick && !disabled) { + onClick(event); + } + }; + + const classNames = cx( + theme['menu-item'], + { + [theme['is-selected']]: selected, + [theme['is-disabled']]: disabled, + }, + className, + ); - const color = destructive ? 'ruby' : disabled ? 'neutral' : 'teal'; - const tint = disabled && destructive ? 'light' : disabled || destructive ? 'dark' : 'darkest'; + const color = destructive ? 'ruby' : disabled ? 'neutral' : 'teal'; + const tint = disabled && destructive ? 'light' : disabled || destructive ? 'dark' : 'darkest'; - const boxProps = pickBoxProps(others); - const restProps = omitBoxProps(others); + const boxProps = pickBoxProps(others); + const restProps = omitBoxProps(others); - return ( - - - {icon && ( - - {icon} - - )} + return ( + - {children} - {label && ( - - {label} - - )} - {caption && ( - - {caption} - + {icon && ( + + {icon} + )} + + {children} + {label && ( + + {label} + + )} + {caption && ( + + {caption} + + )} + - - ); -}; + ); + }, +); + +MenuItem.displayName = 'MenuItem'; export default MenuItem; diff --git a/src/components/message/Message.tsx b/src/components/message/Message.tsx index cf7750cb9..7ac5ca07d 100644 --- a/src/components/message/Message.tsx +++ b/src/components/message/Message.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { forwardRef } from 'react'; import { IconBellMediumOutline, IconCheckmarkBadgedMediumFilled, @@ -71,76 +71,70 @@ export interface MessageProps extends Omit { title?: React.ReactNode; } -const Message: GenericComponent = ({ - children, - inline, - onClose, - primaryAction, - secondaryAction, - showIcon, - status = 'info', - title, - ...others -}) => { - const hasActions = Boolean(primaryAction || secondaryAction); - const IconToRender = iconMap[status]; +const Message: GenericComponent = forwardRef( + ({ children, inline, onClose, primaryAction, secondaryAction, showIcon, status = 'info', title, ...others }, ref) => { + const hasActions = Boolean(primaryAction || secondaryAction); + const IconToRender = iconMap[status]; - return ( - - {status && ( + return ( + + {status && ( + + {showIcon && ( + + + + )} + + )} - {showIcon && ( - - - - )} - - )} - - - {title && ( - - {title} - - )} - {children} - {hasActions && ( - - {secondaryAction &&