From c58d26cab009ce56d9a4f1e01462656cb0ea380d Mon Sep 17 00:00:00 2001 From: evgenitsn Date: Wed, 27 Jul 2022 15:22:41 +0300 Subject: [PATCH] feat(PPDSC-2215): add forwardRef to components 2 --- .../playback-speed-control.tsx | 126 ++-- src/banner/banner-internal.tsx | 269 +++++---- src/banner/banner.tsx | 82 +-- .../base-floating-element.tsx | 409 +++++++------ src/dialog/base-dialog-function.tsx | 152 ++--- src/label/label.tsx | 26 +- src/modal/modal.tsx | 166 ++--- src/ordered-list/ordered-list.tsx | 12 +- src/popover/popover.tsx | 178 +++--- src/standfirst/standfirst.tsx | 13 +- src/tabs/tabs.tsx | 570 +++++++++--------- src/tag/tag.tsx | 37 +- src/title-bar/title-bar.tsx | 106 ++-- src/tooltip/tooltip.tsx | 118 ++-- src/unordered-list/unordered-list.tsx | 14 +- 15 files changed, 1170 insertions(+), 1108 deletions(-) diff --git a/src/audio-player-composable/components/playback-speed-control/playback-speed-control.tsx b/src/audio-player-composable/components/playback-speed-control/playback-speed-control.tsx index d9ec037d4e..b45f38381f 100644 --- a/src/audio-player-composable/components/playback-speed-control/playback-speed-control.tsx +++ b/src/audio-player-composable/components/playback-speed-control/playback-speed-control.tsx @@ -14,76 +14,78 @@ import {Popover} from '../../../popover'; import {iconButtonOverrides, popoverOverrides, modalOverrides} from './utils'; import {ButtonSize} from '../../../button'; -const ThemelessAudioPlayerPlaybackSpeedControl: React.FC = React.memo( - props => { - const theme = useTheme(); +const ThemelessAudioPlayerPlaybackSpeedControl = React.forwardRef< + HTMLHeadingElement, + AudioPlayerPlaybackSpeedControlProps +>((props, ref) => { + const theme = useTheme(); - const {getPlaybackSpeedControlProps} = useAudioPlayerContext(); - const { - overrides, - onChange: setSpeed, - useModal, - playbackSpeed, - buttonSize = ButtonSize.Medium, - } = getPlaybackSpeedControlProps!(props); + const {getPlaybackSpeedControlProps} = useAudioPlayerContext(); + const { + overrides, + onChange: setSpeed, + useModal, + playbackSpeed, + buttonSize = ButtonSize.Medium, + } = getPlaybackSpeedControlProps!(props); - const [isOpen, setIsOpen] = useState(false); + const [isOpen, setIsOpen] = useState(false); - const renderInModal = checkBreakpointProp(useModal, useBreakpointKey()); - const selectedOptionRef = useRef(null); + const renderInModal = checkBreakpointProp(useModal, useBreakpointKey()); + const selectedOptionRef = useRef(null); - const playbackSpeedList = ( - - ); + const playbackSpeedList = ( + + ); - const dismissHandler = useCallback(() => { - setIsOpen(false); - }, []); + const dismissHandler = useCallback(() => { + setIsOpen(false); + }, []); - return ( - <> - + + setIsOpen(open => !open)} + size={buttonSize} > - setIsOpen(open => !open)} - size={buttonSize} - > - - - + + + - {renderInModal && ( - - {playbackSpeedList} - - )} - - ); - }, -); + {renderInModal && ( + + {playbackSpeedList} + + )} + + ); +}); export const AudioPlayerPlaybackSpeedControl = withOwnTheme( ThemelessAudioPlayerPlaybackSpeedControl, diff --git a/src/banner/banner-internal.tsx b/src/banner/banner-internal.tsx index 0477cfe583..c9d261687d 100644 --- a/src/banner/banner-internal.tsx +++ b/src/banner/banner-internal.tsx @@ -25,150 +25,161 @@ import {Cell, Grid} from '../grid'; import {childrenIsString} from '../utils/react-children-utilities'; import {omitLogicalPropsFromOverrides} from '../utils/logical-properties'; -export const BannerInternal: React.FC = ({ - actions, - children, - icon, - overrides, - layout, - closeButtonLabel = 'Close', - onClose, - title, - ...restProps -}) => { - const theme = useTheme(); - const closeButtonStyles: ButtonProps['overrides'] = { - ...deepMerge( - mergeBreakpointObject(Object.keys(theme.breakpoints) as BreakpointKeys[]), - theme.componentDefaults.banner[layout].actions.closeButton, - filterOutFalsyProperties(overrides?.actions?.closeButton), - ), - }; +export const BannerInternal = React.forwardRef< + HTMLDivElement, + BannerInternalProps +>( + ( + { + actions, + children, + icon, + overrides, + layout, + closeButtonLabel = 'Close', + onClose, + title, + ...restProps + }, + ref, + ) => { + const theme = useTheme(); + const closeButtonStyles: ButtonProps['overrides'] = { + ...deepMerge( + mergeBreakpointObject( + Object.keys(theme.breakpoints) as BreakpointKeys[], + ), + theme.componentDefaults.banner[layout].actions.closeButton, + filterOutFalsyProperties(overrides?.actions?.closeButton), + ), + }; - const actionsCount = actions ? actions.length : 0; - const actionKeys = useReactKeys(actionsCount); + const actionsCount = actions ? actions.length : 0; + const actionKeys = useReactKeys(actionsCount); - const actionsSpacing = - overrides?.actions?.spaceInline || - theme.componentDefaults.banner[layout].actions.spaceInline; + const actionsSpacing = + overrides?.actions?.spaceInline || + theme.componentDefaults.banner[layout].actions.spaceInline; - const nonLogicalOverrides = omitLogicalPropsFromOverrides(overrides); + const nonLogicalOverrides = omitLogicalPropsFromOverrides(overrides); - return ( - - - - - + + + - {icon && ( - - {icon} - - )} - - {title && ( - + {icon && ( + - {title} - + {icon} + )} - + {title && ( + + {title} + + )} + + {children} + + + + {(actions?.length || onClose) && ( + - {children} - - - - {(actions?.length || onClose) && ( - - {onClose && layout === 'vertical' && ( - - - - )} - {actions && - actionKeys.length && - actions.map((action, idx) => ( + {onClose && layout === 'vertical' && ( - {renderIfReactComponent(action)} + - ))} - {onClose && layout === 'horizontal' && ( - - - - )} - - )} - - - - - ); -}; + )} + {actions && + actionKeys.length && + actions.map((action, idx) => ( + + {renderIfReactComponent(action)} + + ))} + {onClose && layout === 'horizontal' && ( + + + + )} + + )} + + + + + ); + }, +); diff --git a/src/banner/banner.tsx b/src/banner/banner.tsx index 7e794f7129..fd8a4f1003 100644 --- a/src/banner/banner.tsx +++ b/src/banner/banner.tsx @@ -9,45 +9,51 @@ import defaults from './defaults'; import stylePresets from './style-presets'; import {withOwnTheme} from '../utils/with-own-theme'; -const ThemelessBanner: React.FC = ({ - layout = { - xs: 'vertical', - md: 'horizontal', - }, - ...restProps -}) => { - let layoutHasMQ = false; - const theme = useTheme(); +const ThemelessBanner = React.forwardRef( + ( + { + layout = { + xs: 'vertical', + md: 'horizontal', + }, + ...restProps + }, + ref, + ) => { + let layoutHasMQ = false; + const theme = useTheme(); - let horizontalBreakpoints: MQ = {}; - let verticalBreakpoints: MQ = {}; - if (typeof layout === 'object') { - layoutHasMQ = true; - ({ - verticalBreakpoints, - horizontalBreakpoints, - } = getVisibleBreakpointsForLayout(layout, theme)); - } + let horizontalBreakpoints: MQ = {}; + let verticalBreakpoints: MQ = {}; + if (typeof layout === 'object') { + layoutHasMQ = true; + ({ + verticalBreakpoints, + horizontalBreakpoints, + } = getVisibleBreakpointsForLayout(layout, theme)); + } - return ( - <> - {layoutHasMQ ? ( - <> - - - - - - - - ) : ( - - )} - - ); -}; + return ( + <> + {layoutHasMQ ? ( + <> + + + + + + + + ) : ( + + )} + + ); + }, +); export const Banner = withOwnTheme(ThemelessBanner)({defaults, stylePresets}); diff --git a/src/base-floating-element/base-floating-element.tsx b/src/base-floating-element/base-floating-element.tsx index 5ea50c0410..ae9d1d3bfe 100644 --- a/src/base-floating-element/base-floating-element.tsx +++ b/src/base-floating-element/base-floating-element.tsx @@ -19,223 +19,232 @@ import {showOverridePxWarnings, getOverridePxValue} from './utils'; import {getTransitionDuration} from '../utils'; import {getTransitionClassName} from '../utils/get-transition-class-name'; -export const BaseFloatingElement: React.FC = ({ - children, - content, - placement = 'top', - open: openProp, - overrides, - hidePointer = false, - role, - useInteractions, - buildRefElAriaAttributes, - buildFloatingElAriaAttributes, - path, - onDismiss, - restoreFocusTo, - focusElementRef, - className, - /* istanbul ignore next */ - fallbackBehaviour = ['flip', 'shift'], - boundary, -}) => { - const [open, setOpen] = useControlled({ - controlledValue: openProp, - defaultValue: false, - }); +export const BaseFloatingElement = React.forwardRef< + HTMLDivElement, + BaseFloatingElementProps +>( + ( + { + children, + content, + placement = 'top', + open: openProp, + overrides, + hidePointer = false, + role, + useInteractions, + buildRefElAriaAttributes, + buildFloatingElAriaAttributes, + path, + onDismiss, + restoreFocusTo, + focusElementRef, + className, + /* istanbul ignore next */ + fallbackBehaviour = ['flip', 'shift'], + boundary, + }, + ref, + ) => { + const [open, setOpen] = useControlled({ + controlledValue: openProp, + defaultValue: false, + }); - const theme = useTheme(); - const distance = getOverridePxValue( - path, - {theme, overrides}, - 'distance', - 'distance', - ); - const pointerPadding = getOverridePxValue( - `${path}.pointer`, - {theme, overrides}, - 'pointer.edgeOffset', - 'edgeOffset', - ); + const theme = useTheme(); + const distance = getOverridePxValue( + path, + {theme, overrides}, + 'distance', + 'distance', + ); + const pointerPadding = getOverridePxValue( + `${path}.pointer`, + {theme, overrides}, + 'pointer.edgeOffset', + 'edgeOffset', + ); - useEffect(() => { - showOverridePxWarnings(distance, 'distance'); - showOverridePxWarnings(pointerPadding, 'pointer.edgeOffset'); - }, [distance, pointerPadding]); + useEffect(() => { + showOverridePxWarnings(distance, 'distance'); + showOverridePxWarnings(pointerPadding, 'pointer.edgeOffset'); + }, [distance, pointerPadding]); - const panelRef = useRef(null); - const pointerRef = useRef(null); - const { - x, - y, - reference, - floating, - strategy, - context, - middlewareData: {arrow: {x: pointerX, y: pointerY} = {}}, - placement: statefulPlacement, - refs, - } = useFloating({ - placement, - open, - onOpenChange: isOpen => { - // Clicking on the target icon button when controlled doesn't call this. - if (!isOpen && onDismiss) { - onDismiss(); - } - setOpen(isOpen); - }, - whileElementsMounted: autoUpdate, - middleware: [ - ...(!hidePointer && distance ? [offset(distance)] : []), - ...(fallbackBehaviour.includes('flip') - ? [flip({boundary})] - : /* istanbul ignore next */ []), - ...(fallbackBehaviour.includes('shift') - ? [shift({boundary})] - : /* istanbul ignore next */ []), - ...(!hidePointer - ? [arrow({element: pointerRef, padding: pointerPadding})] - : []), - ], - }); + const panelRef = useRef(null); + const pointerRef = useRef(null); + const { + x, + y, + reference, + floating, + strategy, + context, + middlewareData: {arrow: {x: pointerX, y: pointerY} = {}}, + placement: statefulPlacement, + refs, + } = useFloating({ + placement, + open, + onOpenChange: isOpen => { + // Clicking on the target icon button when controlled doesn't call this. + if (!isOpen && onDismiss) { + onDismiss(); + } + setOpen(isOpen); + }, + whileElementsMounted: autoUpdate, + middleware: [ + ...(!hidePointer && distance ? [offset(distance)] : []), + ...(fallbackBehaviour.includes('flip') + ? [flip({boundary})] + : /* istanbul ignore next */ []), + ...(fallbackBehaviour.includes('shift') + ? [shift({boundary})] + : /* istanbul ignore next */ []), + ...(!hidePointer + ? [arrow({element: pointerRef, padding: pointerPadding})] + : []), + ], + }); - const defaultRefId = `ref-${useId()}`; - const floatingId = `floating-${useId()}`; - const ariaArgs = { - floating: {id: floatingId, open}, - ref: {id: children.props.id || defaultRefId}, - }; - const refElAriaAttributes = buildRefElAriaAttributes(ariaArgs); - const floatingElAriaAttributes = buildFloatingElAriaAttributes(ariaArgs); + const defaultRefId = `ref-${useId()}`; + const floatingId = `floating-${useId()}`; + const ariaArgs = { + floating: {id: floatingId, open}, + ref: {id: children.props.id || defaultRefId}, + }; + const refElAriaAttributes = buildRefElAriaAttributes(ariaArgs); + const floatingElAriaAttributes = buildFloatingElAriaAttributes(ariaArgs); - const contentIsString = typeof content === 'string'; - const {getReferenceProps, getFloatingProps} = useInteractions(context); + const contentIsString = typeof content === 'string'; + const {getReferenceProps, getFloatingProps} = useInteractions(context); - // We need to handle changes to the value of 'open' in a useEffect because: - // - We can't access context.refs in onOpenChange - const isFirstRun = useRef(true); - useEffect(() => { - // Don't update focus on the first render. - if (isFirstRun.current) { - isFirstRun.current = false; - return; - } + // We need to handle changes to the value of 'open' in a useEffect because: + // - We can't access context.refs in onOpenChange + const isFirstRun = useRef(true); + useEffect(() => { + // Don't update focus on the first render. + if (isFirstRun.current) { + isFirstRun.current = false; + return; + } - // We can't use floating-ui's FloatingFocusManager to update the focus state - // because this does not allow tabbing past the floating element without closing it. - if (path === 'popover') { - if (open) { - /* istanbul ignore next */ - if (focusElementRef?.current) { + // We can't use floating-ui's FloatingFocusManager to update the focus state + // because this does not allow tabbing past the floating element without closing it. + if (path === 'popover') { + if (open) { /* istanbul ignore next */ - focusElementRef.current.focus(); + if (focusElementRef?.current) { + /* istanbul ignore next */ + focusElementRef.current.focus(); + } else { + /* istanbul ignore next */ + panelRef?.current?.focus(); + } + } else if (restoreFocusTo) { + restoreFocusTo.focus(); } else { /* istanbul ignore next */ - panelRef?.current?.focus(); + refs.reference?.current?.focus(); } - } else if (restoreFocusTo) { - restoreFocusTo.focus(); - } else { - /* istanbul ignore next */ - refs.reference?.current?.focus(); } - } - }, [ - open, - path, - refs.reference, - panelRef, - focusElementRef, - openProp, - restoreFocusTo, - ]); + }, [ + open, + path, + refs.reference, + panelRef, + focusElementRef, + openProp, + restoreFocusTo, + ]); - if (!content) { - return children; - } + if (!content) { + return children; + } - // This object contains the event handlers that should be added to the reference - // element (e.g. onClick, etc. if useClick is passed to useInteractions). It is - // also passed to the content prop (if this is a function) to allow other elements - // to trigger these handlers (e.g. the Popover's close button triggers the onClick - // handler). - const referenceProps = getReferenceProps(); + // This object contains the event handlers that should be added to the reference + // element (e.g. onClick, etc. if useClick is passed to useInteractions). It is + // also passed to the content prop (if this is a function) to allow other elements + // to trigger these handlers (e.g. the Popover's close button triggers the onClick + // handler). + const referenceProps = getReferenceProps(); - const baseTransitionClassname = `nk-${path}`; + const baseTransitionClassname = `nk-${path}`; - return ( - <> - {React.cloneElement(children, { - ref: composeRefs(reference, children.ref), - ...refElAriaAttributes, - id: - floatingElAriaAttributes['aria-labelledby'] && !children.props.id - ? defaultRefId - : undefined, - ...referenceProps, - // Overriding the referenceProps events and with the user's provided (if any) events. - onClick: children.props.onClick || referenceProps.onClick, - onKeyDown: children.props.onKeyDown || referenceProps.onKeyDown, - onKeyUp: children.props.onKeyUp || referenceProps.onKeyUp, - onPointerDown: - children.props.onPointerDown || referenceProps.onPointerDown, - })} - - {state => ( - - + {React.cloneElement(children, { + ref: composeRefs(reference, children.ref), + ...refElAriaAttributes, + id: + floatingElAriaAttributes['aria-labelledby'] && !children.props.id + ? defaultRefId + : undefined, + ...referenceProps, + // Overriding the referenceProps events and with the user's provided (if any) events. + onClick: children.props.onClick || referenceProps.onClick, + onKeyDown: children.props.onKeyDown || referenceProps.onKeyDown, + onKeyUp: children.props.onKeyUp || referenceProps.onKeyUp, + onPointerDown: + children.props.onPointerDown || referenceProps.onPointerDown, + })} + + {state => ( + - {typeof content === 'function' - ? content(referenceProps) - : content} - - {!hidePointer && ( - - )} - - )} - - - ); -}; + path={path} + ref={panelRef} + > + {typeof content === 'function' + ? content(referenceProps) + : content} + + {!hidePointer && ( + + )} + + )} + + + ); + }, +); diff --git a/src/dialog/base-dialog-function.tsx b/src/dialog/base-dialog-function.tsx index 1f13f96f31..c31370c2ab 100644 --- a/src/dialog/base-dialog-function.tsx +++ b/src/dialog/base-dialog-function.tsx @@ -1,94 +1,104 @@ import React, {useRef, useEffect, useCallback} from 'react'; import FocusLock from 'react-focus-lock'; import {hideOthers, Undo} from 'aria-hidden'; +import composeRefs from '@seznam/compose-react-refs'; import {useKeypress} from '../utils/hooks'; import {get} from '../utils/get'; import {BaseDialogFunctionProps} from './types'; -export const BaseDialogFunction: React.FC = ({ - children, - open, - onDismiss, - restoreFocusTo = undefined, - renderOverlay, - hideOverlay, - disableFocusTrap, -}) => { - const triggerClose = () => open && onDismiss && onDismiss(); +export const BaseDialogFunction = React.forwardRef< + HTMLDivElement, + BaseDialogFunctionProps +>( + ( + { + children, + open, + onDismiss, + restoreFocusTo = undefined, + renderOverlay, + hideOverlay, + disableFocusTrap, + }, + ref, + ) => { + const triggerClose = () => open && onDismiss && onDismiss(); - const handleEscape = () => { - triggerClose(); - }; + const handleEscape = () => { + triggerClose(); + }; - const handleOverlayClick = () => { - triggerClose(); - }; + const handleOverlayClick = () => { + triggerClose(); + }; - const handleCloseButtonClick = () => { - triggerClose(); - }; + const handleCloseButtonClick = () => { + triggerClose(); + }; - useKeypress('Escape', handleEscape, {enabled: open}); + useKeypress('Escape', handleEscape, {enabled: open}); - // ref to store activeElement ( focused ) before dialog been opened - const originalFocusedElementRef = useRef(null); + // ref to store activeElement ( focused ) before dialog been opened + const originalFocusedElementRef = useRef(null); - const baseDialogFunctionRef = useRef(null); - const undoRef = useRef<{undo?: Undo}>({}); + const baseDialogFunctionRef = useRef(null); + const undoRef = useRef<{undo?: Undo}>({}); - const handleOnLockActivation = () => { - originalFocusedElementRef.current = document.activeElement; + const handleOnLockActivation = () => { + originalFocusedElementRef.current = document.activeElement; - // Aria hides everything else that is not contained inside BaseDialogFunction - if (baseDialogFunctionRef.current) { - const undo = hideOthers(baseDialogFunctionRef.current); - undoRef.current = {undo}; - } - }; + // Aria hides everything else that is not contained inside BaseDialogFunction + if (baseDialogFunctionRef.current) { + const undo = hideOthers(baseDialogFunctionRef.current); + undoRef.current = {undo}; + } + }; - const handleOnLockDeactivation = useCallback(() => { - const originalFocusedElement = get(originalFocusedElementRef, 'current'); + const handleOnLockDeactivation = useCallback(() => { + const originalFocusedElement = get(originalFocusedElementRef, 'current'); - /* istanbul ignore else */ - if (restoreFocusTo || originalFocusedElement) { - const elementToFocus = restoreFocusTo || originalFocusedElement; - // without the zero-timeout, focus will likely remain on the dialog - window.setTimeout( - () => - typeof elementToFocus.focus === 'function' && elementToFocus.focus(), - 0, - ); - } + /* istanbul ignore else */ + if (restoreFocusTo || originalFocusedElement) { + const elementToFocus = restoreFocusTo || originalFocusedElement; + // without the zero-timeout, focus will likely remain on the dialog + window.setTimeout( + () => + typeof elementToFocus.focus === 'function' && + elementToFocus.focus(), + 0, + ); + } - if (undoRef.current) { - const {undo} = undoRef.current; - if (typeof undo === 'function') { - undo(); + if (undoRef.current) { + const {undo} = undoRef.current; + if (typeof undo === 'function') { + undo(); + } } - } - }, [restoreFocusTo]); + }, [restoreFocusTo]); - useEffect(() => { - if (disableFocusTrap && open) { - handleOnLockActivation(); - } - return () => { + useEffect(() => { if (disableFocusTrap && open) { - handleOnLockDeactivation(); + handleOnLockActivation(); } - }; - }, [disableFocusTrap, handleOnLockDeactivation, open]); + return () => { + if (disableFocusTrap && open) { + handleOnLockDeactivation(); + } + }; + }, [disableFocusTrap, handleOnLockDeactivation, open]); - return ( -
- {!hideOverlay && renderOverlay(handleOverlayClick)} - - {children && children(handleCloseButtonClick)} - -
- ); -}; + return ( +
+ {!hideOverlay && renderOverlay(handleOverlayClick)} + + {children && children(handleCloseButtonClick)} + +
+ ); + }, +); diff --git a/src/label/label.tsx b/src/label/label.tsx index dafc4ec207..c1ef121846 100644 --- a/src/label/label.tsx +++ b/src/label/label.tsx @@ -6,20 +6,18 @@ import defaults from './defaults'; import stylePresets from './style-presets'; import {withOwnTheme} from '../utils/with-own-theme'; -const ThemelessLabel = ({ - size = 'medium' as TextFieldSize, - children, - state, - ...props -}: LabelProps) => ( - - {children} - +const ThemelessLabel = React.forwardRef( + ({size = 'medium' as TextFieldSize, children, state, ...props}, ref) => ( + + {children} + + ), ); export const Label = withOwnTheme(ThemelessLabel)({defaults, stylePresets}); diff --git a/src/modal/modal.tsx b/src/modal/modal.tsx index 6427d81776..749cbd90ab 100644 --- a/src/modal/modal.tsx +++ b/src/modal/modal.tsx @@ -15,90 +15,98 @@ import {withOwnTheme} from '../utils/with-own-theme'; import {Layer} from '../layer'; import {getTransitionClassName} from '../utils/get-transition-class-name'; -const ThemelessModal: React.FC = ({ - children, - /* istanbul ignore next */ - open = false, - onDismiss, - restoreFocusTo, - closePosition = 'right', - overrides, - hideOverlay, - disableFocusTrap, - ...props -}) => { - const theme = useTheme(); +const ThemelessModal = React.forwardRef( + ( + { + children, + /* istanbul ignore next */ + open = false, + onDismiss, + restoreFocusTo, + closePosition = 'right', + overrides, + hideOverlay, + disableFocusTrap, + ...props + }, + ref, + ) => { + const theme = useTheme(); - const overlayOverrides = { - ...deepMerge( - mergeBreakpointObject(Object.keys(theme.breakpoints) as BreakpointKeys[]), - theme.componentDefaults.modal.overlay, - filterOutFalsyProperties(overrides && overrides.overlay), - ), - }; + const overlayOverrides = { + ...deepMerge( + mergeBreakpointObject( + Object.keys(theme.breakpoints) as BreakpointKeys[], + ), + theme.componentDefaults.modal.overlay, + filterOutFalsyProperties(overrides && overrides.overlay), + ), + }; - const [showWrapper, setShowWrapper] = React.useState(false); + const [showWrapper, setShowWrapper] = React.useState(false); - // When Modal is used inline, it should not be in a layer - const OuterWrapper = props.inline ? React.Fragment : Layer; + // When Modal is used inline, it should not be in a layer + const OuterWrapper = props.inline ? React.Fragment : Layer; - return ( - - ( - - )} - > - {handleCloseButtonClick => ( - - setShowWrapper(true)} - onExited={() => setShowWrapper(false)} + return ( + + ( + + )} + > + {handleCloseButtonClick => ( + - {state => ( - - {children} - - )} - - - )} - - - ); -}; + setShowWrapper(true)} + onExited={() => setShowWrapper(false)} + > + {state => ( + + {children} + + )} + + + )} + + + ); + }, +); export const Modal = withOwnTheme(ThemelessModal)({ defaults, diff --git a/src/ordered-list/ordered-list.tsx b/src/ordered-list/ordered-list.tsx index 0fc4499dc8..a74a57c1eb 100644 --- a/src/ordered-list/ordered-list.tsx +++ b/src/ordered-list/ordered-list.tsx @@ -37,17 +37,17 @@ const List = styled.ol>` ${logicalProps()} `; -const ThemelessOrderedList: React.FC = ({ - children, - overrides, -}) => ( - +const ThemelessOrderedList = React.forwardRef< + HTMLOListElement, + OrderedListProps +>(({children, overrides}, ref) => ( + {React.Children.map(children, node => isValidNode(node) ? ( {node} ) : null, )} -); +)); export const OrderedList = withOwnTheme(ThemelessOrderedList)({defaults}); diff --git a/src/popover/popover.tsx b/src/popover/popover.tsx index 972d00c652..04d04db450 100644 --- a/src/popover/popover.tsx +++ b/src/popover/popover.tsx @@ -33,96 +33,104 @@ const buildContextAriaAttributes: BuildAriaAttributesFn = ({ 'aria-controls': open ? id : undefined, }); -const ThemelessPopover: React.FC = ({ - children, - content, - header, - closePosition = 'right', - overrides = {}, - handleCloseButtonClick, - enableDismiss = false, - ...props -}) => { - const theme = useTheme(); - const closeButtonOverrides: typeof overrides['closeButton'] = { - ...deepMerge( - mergeBreakpointObject(Object.keys(theme.breakpoints) as BreakpointKeys[]), - theme.componentDefaults.popover.closeButton, - filterOutFalsyProperties(overrides.closeButton), - ), - }; - const headerId = `header-${useId()}`; +const ThemelessPopover = React.forwardRef( + ( + { + children, + content, + header, + closePosition = 'right', + overrides = {}, + handleCloseButtonClick, + enableDismiss = false, + ...props + }, + ref, + ) => { + const theme = useTheme(); + const closeButtonOverrides: typeof overrides['closeButton'] = { + ...deepMerge( + mergeBreakpointObject( + Object.keys(theme.breakpoints) as BreakpointKeys[], + ), + theme.componentDefaults.popover.closeButton, + filterOutFalsyProperties(overrides.closeButton), + ), + }; + const headerId = `header-${useId()}`; - const buildFloatingElementAriaAttributes: BuildAriaAttributesFn = ({ - ref: {id}, - }) => ({ - 'aria-labelledby': header ? undefined : id, - 'aria-describedby': header ? headerId : undefined, - }); + const buildFloatingElementAriaAttributes: BuildAriaAttributesFn = ({ + ref: {id}, + }) => ({ + 'aria-labelledby': header ? undefined : id, + 'aria-describedby': header ? headerId : undefined, + }); - const useInteractions = (context: FloatingContext) => - floatingUiUseInteractions([ - useClick(context), - useDismiss(context, { - enabled: enableDismiss, - }), - ]); + const useInteractions = (context: FloatingContext) => + floatingUiUseInteractions([ + useClick(context), + useDismiss(context, { + enabled: enableDismiss, + }), + ]); - if (!content) { - return children; - } + if (!content) { + return children; + } - return ( - ( - - {header !== undefined && ( - - {header} - - )} - - {content} - - {closePosition !== 'none' && ( - - ) => { - onClick(e); - if (handleCloseButtonClick) { - handleCloseButtonClick(); - } - }} - data-testid="close-button" - aria-label="close" - overrides={closeButtonOverrides} - size={ButtonSize.Medium} + return ( + ( + + {header !== undefined && ( + - - - - )} - - )} - buildRefElAriaAttributes={buildContextAriaAttributes} - buildFloatingElAriaAttributes={buildFloatingElementAriaAttributes} - useInteractions={useInteractions} - role="dialog" - overrides={overrides} - {...props} - > - {children} - - ); -}; + {header} + + )} + + {content} + + {closePosition !== 'none' && ( + + ) => { + onClick(e); + if (handleCloseButtonClick) { + handleCloseButtonClick(); + } + }} + data-testid="close-button" + aria-label="close" + overrides={closeButtonOverrides} + size={ButtonSize.Medium} + > + + + + )} + + )} + buildRefElAriaAttributes={buildContextAriaAttributes} + buildFloatingElAriaAttributes={buildFloatingElementAriaAttributes} + useInteractions={useInteractions} + role="dialog" + overrides={overrides} + {...props} + > + {children} + + ); + }, +); export const Popover = withOwnTheme(ThemelessPopover)({ defaults, diff --git a/src/standfirst/standfirst.tsx b/src/standfirst/standfirst.tsx index 83f224e819..2f12d390d6 100644 --- a/src/standfirst/standfirst.tsx +++ b/src/standfirst/standfirst.tsx @@ -16,15 +16,14 @@ const StyledText = styled.h2` ${({as}) => as && (isInlineElement(as) ? 'display: inline-block' : '')} `; -const ThemelessStandfirst: React.FC = ({ - children, - as, - overrides = {}, -}) => ( - +const ThemelessStandfirst = React.forwardRef< + HTMLHeadingElement, + StandfirstProps +>(({children, as, overrides = {}}, ref) => ( + {children} -); +)); export const Standfirst = withOwnTheme(ThemelessStandfirst)({ defaults, diff --git a/src/tabs/tabs.tsx b/src/tabs/tabs.tsx index 11e5033b2d..428df03ecd 100644 --- a/src/tabs/tabs.tsx +++ b/src/tabs/tabs.tsx @@ -68,318 +68,324 @@ const DefaultScroll = withDefaultProps( 'tabs.scroll', ); -const ThemelessTabs: React.FC = ({ - children, - overrides = {}, - size = TabSize.Medium, - divider, - vertical = false, - distribution, - selectedIndex, - initialSelectedIndex = 0, - indicatorPosition = TabsIndicatorPosition.End, - align: passedAlign, - onChange, -}) => { - const theme = useTheme(); - const align = getAlign(passedAlign, vertical); - const nonLogicalOverrides = omitLogicalPropsFromOverrides(overrides); - - const [ScrollComponent, scrollProps] = getComponentOverrides( - /* istanbul ignore next */ - overrides?.scroll, - DefaultScroll, +const ThemelessTabs = React.forwardRef( + ( { - vertical, - tabIndex: undefined, + children, + overrides = {}, + size = TabSize.Medium, + divider, + vertical = false, + distribution, + selectedIndex, + initialSelectedIndex = 0, + indicatorPosition = TabsIndicatorPosition.End, + align: passedAlign, + onChange, }, - ); - - // filter out children which are not Tab component - const tabsOnlyChildren = React.Children.toArray( - children, - ).filter((child: React.ReactNode) => - hasMatchingDisplayNameWith(child, Tab), - ) as Array; - - // The index of the active tab - this is what we change on click to trigger a visual tab change - const [activeTabIndex, setActiveTabIndex] = useState(() => - validateInitialSelectedIndex( - selectedIndex || initialSelectedIndex, + ref, + ) => { + const theme = useTheme(); + const align = getAlign(passedAlign, vertical); + const nonLogicalOverrides = omitLogicalPropsFromOverrides(overrides); + + const [ScrollComponent, scrollProps] = getComponentOverrides( + /* istanbul ignore next */ + overrides?.scroll, + DefaultScroll, + { + vertical, + tabIndex: undefined, + }, + ); + + // filter out children which are not Tab component + const tabsOnlyChildren = React.Children.toArray( children, - ), - ); - - useEffect(() => { - if (selectedIndex !== undefined) { - setActiveTabIndex(validateSelectedIndex(selectedIndex, children)); - } - }, [selectedIndex, children]); - - const changeActiveTab = (newSelectedIndex: number) => { - if (selectedIndex === undefined) { - setActiveTabIndex(newSelectedIndex); - } - if (onChange && typeof onChange === 'function') { - onChange(newSelectedIndex); - } - }; - const [indicator, setIndicator] = useState({ - size: 0, - distance: 0, - }); - - // Just an incremental counter to trigger re-renders when the tab is changed (active tab ref changing wont trigger a render) - const [keyUpdated, setKeyUpdated] = useState(0); - React.useEffect(() => { - setKeyUpdated(keyUpdated + 1); - }, [activeTabIndex]); // eslint-disable-line react-hooks/exhaustive-deps - - const activeTabRef = React.useRef(null); - // Reference like this so linter does not remove from hooks dependencies - const currentActiveTabRef = activeTabRef.current; - - const tabsBarTrackRef = React.useRef(null); - const [tabsBarTrackWidth, tabsBarTrackHeight] = useResizeObserver( - tabsBarTrackRef, - ); - const tabsBarTrackSize = vertical ? tabsBarTrackHeight : tabsBarTrackWidth; - - const [activeTabWidth, activeTabHeight] = useResizeObserver(activeTabRef); - const activeTabSize = vertical ? activeTabHeight : activeTabWidth; - - const tabsBarIndicatorSizeOverride = get( - nonLogicalOverrides, - 'selectionIndicator.indicator.size', - ); - - React.useEffect(() => { - if (currentActiveTabRef) { - setIndicator( - getLayoutParams( - currentActiveTabRef, - theme, - vertical, - tabsBarIndicatorSizeOverride, - ), - ); - } - }, [ - currentActiveTabRef, - tabsBarIndicatorSizeOverride, - theme, - vertical, - tabsBarTrackSize, - activeTabSize, - ]); - - const handleKeyDown = (event: React.KeyboardEvent) => { - // WAI-ARIA 1.1 - // https://www.w3.org/TR/wai-aria-practices-1.1/#tabpanel - // We use directional keys to iterate focus through Tabs. - - // Find all tabs eligible for focus - const availableTabs: HTMLButtonElement[] = []; - - const tabListElement = getFirstParentElementWithRole( - event.currentTarget, - 'tablist', + ).filter((child: React.ReactNode) => + hasMatchingDisplayNameWith(child, Tab), + ) as Array; + + // The index of the active tab - this is what we change on click to trigger a visual tab change + const [activeTabIndex, setActiveTabIndex] = useState(() => + validateInitialSelectedIndex( + selectedIndex || initialSelectedIndex, + children, + ), ); - Array.from(tabListElement.childNodes).forEach(innerNode => { - const element = getDescendantOnlyFromFirstChild( - innerNode, - 'tab', - ) as HTMLButtonElement; + useEffect(() => { + if (selectedIndex !== undefined) { + setActiveTabIndex(validateSelectedIndex(selectedIndex, children)); + } + }, [selectedIndex, children]); - if (element && !element.disabled) { - availableTabs.push(element); + const changeActiveTab = (newSelectedIndex: number) => { + if (selectedIndex === undefined) { + setActiveTabIndex(newSelectedIndex); + } + if (onChange && typeof onChange === 'function') { + onChange(newSelectedIndex); } + }; + const [indicator, setIndicator] = useState({ + size: 0, + distance: 0, }); - // Exit early if there are no other tabs available - if (availableTabs.length <= 1) return; + // Just an incremental counter to trigger re-renders when the tab is changed (active tab ref changing wont trigger a render) + const [keyUpdated, setKeyUpdated] = useState(0); + React.useEffect(() => { + setKeyUpdated(keyUpdated + 1); + }, [activeTabIndex]); // eslint-disable-line react-hooks/exhaustive-deps - // Find tab to focus, looping to start/end of list if necessary - const currentTabIndex = availableTabs.indexOf(event.currentTarget); - const action = parseKeyDown(event, vertical); + const activeTabRef = React.useRef(null); + // Reference like this so linter does not remove from hooks dependencies + const currentActiveTabRef = activeTabRef.current; - if (!action) return; + const tabsBarTrackRef = React.useRef(null); + const [tabsBarTrackWidth, tabsBarTrackHeight] = useResizeObserver( + tabsBarTrackRef, + ); + const tabsBarTrackSize = vertical ? tabsBarTrackHeight : tabsBarTrackWidth; - // prevent scrolling when you switch tabs using arrows - event.preventDefault(); + const [activeTabWidth, activeTabHeight] = useResizeObserver(activeTabRef); + const activeTabSize = vertical ? activeTabHeight : activeTabWidth; - const keyboardActions = { - [KEYBOARD_ACTION.previous]: - availableTabs[currentTabIndex - 1] || - availableTabs[availableTabs.length - 1], - [KEYBOARD_ACTION.next]: - availableTabs[currentTabIndex + 1] || availableTabs[0], - [KEYBOARD_ACTION.start]: availableTabs[0], - [KEYBOARD_ACTION.end]: availableTabs[availableTabs.length - 1], - }; + const tabsBarIndicatorSizeOverride = get( + nonLogicalOverrides, + 'selectionIndicator.indicator.size', + ); - const nextTab = keyboardActions[action]; + React.useEffect(() => { + if (currentActiveTabRef) { + setIndicator( + getLayoutParams( + currentActiveTabRef, + theme, + vertical, + tabsBarIndicatorSizeOverride, + ), + ); + } + }, [ + currentActiveTabRef, + tabsBarIndicatorSizeOverride, + theme, + vertical, + tabsBarTrackSize, + activeTabSize, + ]); - /* istanbul ignore if */ - if (!nextTab) { - return; - } + const handleKeyDown = (event: React.KeyboardEvent) => { + // WAI-ARIA 1.1 + // https://www.w3.org/TR/wai-aria-practices-1.1/#tabpanel + // We use directional keys to iterate focus through Tabs. - // Focus the tab - nextTab.focus(); - nextTab.click(); - /* istanbul ignore next */ - if ('scrollIntoView' in nextTab) { - requestAnimationFrame(() => { - nextTab.scrollIntoView(); + // Find all tabs eligible for focus + const availableTabs: HTMLButtonElement[] = []; + + const tabListElement = getFirstParentElementWithRole( + event.currentTarget, + 'tablist', + ); + + Array.from(tabListElement.childNodes).forEach(innerNode => { + const element = getDescendantOnlyFromFirstChild( + innerNode, + 'tab', + ) as HTMLButtonElement; + + if (element && !element.disabled) { + availableTabs.push(element); + } }); - } - }; - // generate uniq IDs for a11y purposes - const ariaIds = useReactKeys(tabsOnlyChildren.length); + // Exit early if there are no other tabs available + if (availableTabs.length <= 1) return; - const tabPanels = tabsOnlyChildren.map( - (child: React.ReactElement, index) => { - /* istanbul ignore next */ - const key = child.key || `panel-${index}`; - const tabPanelProps = { - key, - children: child.props.children, - selected: index === activeTabIndex, - id: ariaIds[index], + // Find tab to focus, looping to start/end of list if necessary + const currentTabIndex = availableTabs.indexOf(event.currentTarget); + const action = parseKeyDown(event, vertical); + + if (!action) return; + + // prevent scrolling when you switch tabs using arrows + event.preventDefault(); + + const keyboardActions = { + [KEYBOARD_ACTION.previous]: + availableTabs[currentTabIndex - 1] || + availableTabs[availableTabs.length - 1], + [KEYBOARD_ACTION.next]: + availableTabs[currentTabIndex + 1] || availableTabs[0], + [KEYBOARD_ACTION.start]: availableTabs[0], + [KEYBOARD_ACTION.end]: availableTabs[availableTabs.length - 1], }; - return ; - }, - ); + const nextTab = keyboardActions[action]; - const tabData = tabsOnlyChildren.map( - (child: React.ReactElement, index) => { + /* istanbul ignore if */ + if (!nextTab) { + return; + } + + // Focus the tab + nextTab.focus(); + nextTab.click(); /* istanbul ignore next */ - const key = child.key || `tab-${index}`; - return { - key, - selected: index === activeTabIndex, - id: ariaIds[index], - ...child.props, - }; - }, - ); - - const addStackDivider = (key: React.Key) => ( - - - - ); - - const getChildren = ( - tab: string | React.ReactNode, - ): string | React.ReactNode => { - if (isFragment(tab)) { - // un-wrap the fragment from Tab.label prop - return tab.props.children; - } - return tab; - }; - const tabs = tabData.reduce((acc, tab, index, array) => { - acc.push( - { + nextTab.scrollIntoView(); + }); + } + }; + + // generate uniq IDs for a11y purposes + const ariaIds = useReactKeys(tabsOnlyChildren.length); + + const tabPanels = tabsOnlyChildren.map( + (child: React.ReactElement, index) => { + /* istanbul ignore next */ + const key = child.key || `panel-${index}`; + const tabPanelProps = { + key, + children: child.props.children, + selected: index === activeTabIndex, + id: ariaIds[index], + }; + + return ; + }, + ); + + const tabData = tabsOnlyChildren.map( + (child: React.ReactElement, index) => { + /* istanbul ignore next */ + const key = child.key || `tab-${index}`; + return { + key, + selected: index === activeTabIndex, + id: ariaIds[index], + ...child.props, + }; + }, + ); + + const addStackDivider = (key: React.Key) => ( + - - changeActiveTab(index)} - disabled={tab.disabled} - ref={tab.selected ? activeTabRef : undefined} - id={tab.id} - align={align} - ariaLabel={tab.ariaLabel} - overrides={{ - ...tab.overrides, - width: '100%', - height: vertical ? '100%' : '', - }} - > - {getChildren(tab.label)} - - - , + + ); - if (divider && index < array.length - 1) { - acc.push(addStackDivider(tab.key)); - } - return acc; - }, [] as React.ReactElement[]); - - return ( - - { + if (isFragment(tab)) { + // un-wrap the fragment from Tab.label prop + return tab.props.children; + } + return tab; + }; + const tabs = tabData.reduce((acc, tab, index, array) => { + acc.push( + + + changeActiveTab(index)} + disabled={tab.disabled} + ref={tab.selected ? activeTabRef : undefined} + id={tab.id} + align={align} + ariaLabel={tab.ariaLabel} + overrides={{ + ...tab.overrides, + width: '100%', + height: vertical ? '100%' : '', + }} + > + {getChildren(tab.label)} + + + , + ); + + if (divider && index < array.length - 1) { + acc.push(addStackDivider(tab.key)); + } + return acc; + }, [] as React.ReactElement[]); + + return ( + - - - {tabs} - - - - - - {tabPanels} - - ); -}; + data-testid="tab-bar" + > + + + {tabs} + + + + + + {tabPanels} + + ); + }, +); export const Tabs = withOwnTheme(ThemelessTabs)({defaults, stylePresets}); diff --git a/src/tag/tag.tsx b/src/tag/tag.tsx index 4b78c50f90..a2040857d8 100644 --- a/src/tag/tag.tsx +++ b/src/tag/tag.tsx @@ -15,23 +15,26 @@ const StyledFlag = styled(Flag)` ${({size}) => getTransitionPreset(`tag.${size}`, '')}; `; -const ThemelessTag = ({overrides = {}, disabled, href, ...props}: TagProps) => { - const theme = useTheme(); - const {size = TagSize.Medium} = props; +const ThemelessTag = React.forwardRef( + ({overrides = {}, disabled, href, ...props}, ref) => { + const theme = useTheme(); + const {size = TagSize.Medium} = props; - return ( - - ); -}; + return ( + + ); + }, +); export const Tag = withOwnTheme(ThemelessTag)({defaults, stylePresets}); diff --git a/src/title-bar/title-bar.tsx b/src/title-bar/title-bar.tsx index 9ea7b74142..272df6e333 100644 --- a/src/title-bar/title-bar.tsx +++ b/src/title-bar/title-bar.tsx @@ -9,65 +9,69 @@ import stylePresets from './style-presets'; import {withOwnTheme} from '../utils/with-own-theme'; import {StyledBlock, StyledStackContainer} from './styled'; -const ThemelessTitleBar: React.FC = props => { - const { - children, - hideActionItemOn = {xs: true}, - headingAs = 'h3', - actionItem: ActionItem, - overrides = {}, - } = props; +const ThemelessTitleBar = React.forwardRef( + (props, ref) => { + const { + children, + hideActionItemOn = {xs: true}, + headingAs = 'h3', + actionItem: ActionItem, + overrides = {}, + } = props; - const theme = useTheme(); + const theme = useTheme(); - const hasActions = !!ActionItem; + const hasActions = !!ActionItem; - const addTitleBarHeadingOverrides = () => { - const headingOverrides: Omit = {}; - if (!overrides.heading) { + const addTitleBarHeadingOverrides = () => { + const headingOverrides: Omit = {}; + if (!overrides.heading) { + return headingOverrides; + } + if (overrides.heading.typographyPreset) { + headingOverrides.typographyPreset = overrides.heading.typographyPreset; + } + if (overrides.heading.stylePreset) { + headingOverrides.heading = {stylePreset: overrides.heading.stylePreset}; + } return headingOverrides; - } - if (overrides.heading.typographyPreset) { - headingOverrides.typographyPreset = overrides.heading.typographyPreset; - } - if (overrides.heading.stylePreset) { - headingOverrides.heading = {stylePreset: overrides.heading.stylePreset}; - } - return headingOverrides; - }; + }; - const headlineOverrides = { - typographyPreset: { - ...theme.componentDefaults.titleBar.heading.typographyPreset, - }, - heading: { - stylePreset: theme.componentDefaults.titleBar.heading.stylePreset, - }, + const headlineOverrides = { + typographyPreset: { + ...theme.componentDefaults.titleBar.heading.typographyPreset, + }, + heading: { + stylePreset: theme.componentDefaults.titleBar.heading.stylePreset, + }, - ...addTitleBarHeadingOverrides(), - }; + ...addTitleBarHeadingOverrides(), + }; - const blockOverrides = {spaceInline: hasActions ? 'space040' : ''}; + const blockOverrides = {spaceInline: hasActions ? 'space040' : ''}; + + return ( + + + + {children} + + + {ActionItem && ( + + + + )} + + ); + }, +); - return ( - - - - {children} - - - {ActionItem && ( - - - - )} - - ); -}; export const TitleBar = withOwnTheme(ThemelessTitleBar)({ defaults, stylePresets, diff --git a/src/tooltip/tooltip.tsx b/src/tooltip/tooltip.tsx index 6162e71886..2d5d79ff73 100644 --- a/src/tooltip/tooltip.tsx +++ b/src/tooltip/tooltip.tsx @@ -14,72 +14,72 @@ import stylePresets from './style-presets'; import {BaseFloatingElement} from '../base-floating-element/base-floating-element'; import {BuildAriaAttributesFn} from '../base-floating-element'; -const ThemelessTooltip: React.FC = ({ - children, - content, - trigger = ['hover', 'focus'], - asLabel, - ...props -}) => { - const useInteractions = (context: FloatingContext) => - floatingUiUseInteractions([ - useHover(context, { - enabled: trigger.includes('hover'), - }), - useFocus(context, {enabled: trigger.includes('focus')}), - useDismiss(context), - ]); +const ThemelessTooltip = React.forwardRef( + ( + {children, content, trigger = ['hover', 'focus'], asLabel, ...props}, + ref, + ) => { + const useInteractions = (context: FloatingContext) => + floatingUiUseInteractions([ + useHover(context, { + enabled: trigger.includes('hover'), + }), + useFocus(context, {enabled: trigger.includes('focus')}), + useDismiss(context), + ]); - const contentIsString = typeof content === 'string'; + const contentIsString = typeof content === 'string'; - const showDisabledWarning = (): void => { - if (process.env.NODE_ENV !== 'production' && children.props.disabled) { - // eslint-disable-next-line no-console - console.warn( - `When passing a component with disabled prop to Tooltip please remember to use a wrapper element, such as a span.`, - ); - } - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => showDisabledWarning(), [children.props.disabled]); + const showDisabledWarning = (): void => { + if (process.env.NODE_ENV !== 'production' && children.props.disabled) { + // eslint-disable-next-line no-console + console.warn( + `When passing a component with disabled prop to Tooltip please remember to use a wrapper element, such as a span.`, + ); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => showDisabledWarning(), [children.props.disabled]); - const buildContextAriaAttributes: BuildAriaAttributesFn = ({ - floating: {id, open}, - }) => { - const attrs: AriaAttributes = {}; + const buildContextAriaAttributes: BuildAriaAttributesFn = ({ + floating: {id, open}, + }) => { + const attrs: AriaAttributes = {}; - // If tooltip is used as a label, add aria-label or aria-labelledby to childrenProps and id to StyledTooltip; - // aria-label is used when content is string; aria-labelledby is used when it's not a string; - // Because of above, 'aria-describedby' has different id for reference and floating, hence manually set below as well; - if (asLabel) { - attrs['aria-label'] = contentIsString ? content : undefined; - attrs['aria-labelledby'] = open && !contentIsString ? id : undefined; - } else { - attrs['aria-describedby'] = open ? id : undefined; - } + // If tooltip is used as a label, add aria-label or aria-labelledby to childrenProps and id to StyledTooltip; + // aria-label is used when content is string; aria-labelledby is used when it's not a string; + // Because of above, 'aria-describedby' has different id for reference and floating, hence manually set below as well; + if (asLabel) { + attrs['aria-label'] = contentIsString ? content : undefined; + attrs['aria-labelledby'] = open && !contentIsString ? id : undefined; + } else { + attrs['aria-describedby'] = open ? id : undefined; + } - return attrs; - }; + return attrs; + }; - const buildFloatingElementAriaAttributes: BuildAriaAttributesFn = () => ({ - 'aria-hidden': true, - }); + const buildFloatingElementAriaAttributes: BuildAriaAttributesFn = () => ({ + 'aria-hidden': true, + }); - return ( - - {children} - - ); -}; + return ( + + {children} + + ); + }, +); export const Tooltip = withOwnTheme(ThemelessTooltip)({ defaults, diff --git a/src/unordered-list/unordered-list.tsx b/src/unordered-list/unordered-list.tsx index ba86b3f759..0c38c7ad40 100644 --- a/src/unordered-list/unordered-list.tsx +++ b/src/unordered-list/unordered-list.tsx @@ -14,12 +14,10 @@ import { import defaults from './defaults'; import {withOwnTheme} from '../utils/with-own-theme'; -const ThemelessUnorderedList: React.FC = ({ - children, - listItemMarker: ListItemMarker, - markerAlign, - overrides, -}) => { +const ThemelessUnorderedList = React.forwardRef< + HTMLUListElement, + UnorderedListProps +>(({children, listItemMarker: ListItemMarker, markerAlign, overrides}, ref) => { const theme = useTheme(); const itemSpaceToken = getToken( {theme, overrides}, @@ -59,7 +57,7 @@ const ThemelessUnorderedList: React.FC = ({ ); return ( - + {React.Children.map(children, node => { if (!isValidNode(node)) return null; @@ -92,7 +90,7 @@ const ThemelessUnorderedList: React.FC = ({ })} ); -}; +}); export const UnorderedList = withOwnTheme(ThemelessUnorderedList)({ defaults,