diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f607d076..fac877891 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Import types and constants from `@lumx/core` and re-export - Import `className` utilities from `@lumx/core` - Migrate tests to `vitest` + - Uniformize link and button handling in `Link`, `Button`, `SideNavigationItem`, `Thumbnail` and `NavigationItem`. + - Render disabled links as link instead of disabled buttons. ## [3.19.0][] - 2025-11-07 diff --git a/packages/lumx-react/src/components/button/Button.test.tsx b/packages/lumx-react/src/components/button/Button.test.tsx index 67149d6de..9dc006608 100644 --- a/packages/lumx-react/src/components/button/Button.test.tsx +++ b/packages/lumx-react/src/components/button/Button.test.tsx @@ -78,9 +78,10 @@ describe(`<${Button.displayName}>`, () => { it('should render disabled link', async () => { const onClick = vi.fn(); const { button } = setup({ children: 'Label', disabled: true, href: 'https://example.com', onClick }); - // Disabled link do not exist so we fallback to a button - expect(screen.queryByRole('link')).not.toBeInTheDocument(); - expect(button).toHaveAttribute('disabled'); + expect(screen.queryByRole('link')).toBeInTheDocument(); + expect(button).toHaveAttribute('aria-disabled', 'true'); + // Simulate standard disabled state (not focusable) + expect(button).toHaveAttribute('tabindex', '-1'); await userEvent.click(button); expect(onClick).not.toHaveBeenCalled(); }); @@ -102,8 +103,7 @@ describe(`<${Button.displayName}>`, () => { onClick, }); expect(button).toHaveAccessibleName('Label'); - // Disabled link do not exist so we fallback to a button - expect(screen.queryByRole('link')).not.toBeInTheDocument(); + expect(screen.queryByRole('link')).toBeInTheDocument(); expect(button).toHaveAttribute('aria-disabled', 'true'); await userEvent.click(button); expect(onClick).not.toHaveBeenCalled(); diff --git a/packages/lumx-react/src/components/button/ButtonRoot.tsx b/packages/lumx-react/src/components/button/ButtonRoot.tsx index ac200abda..6c5ed2563 100644 --- a/packages/lumx-react/src/components/button/ButtonRoot.tsx +++ b/packages/lumx-react/src/components/button/ButtonRoot.tsx @@ -1,17 +1,15 @@ import React, { AriaAttributes, ButtonHTMLAttributes, DetailedHTMLProps, RefObject } from 'react'; -import isEmpty from 'lodash/isEmpty'; - import classNames from 'classnames'; import { ColorPalette, Emphasis, Size, Theme } from '@lumx/react'; import { CSS_PREFIX } from '@lumx/react/constants'; import { GenericProps, HasTheme } from '@lumx/react/utils/type'; import { handleBasicClasses } from '@lumx/core/js/utils/className'; -import { renderLink } from '@lumx/react/utils/react/renderLink'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; -import { useDisableStateProps } from '@lumx/react/utils/disabled/useDisableStateProps'; import { HasAriaDisabled } from '@lumx/react/utils/type/HasAriaDisabled'; +import { RawClickable } from '@lumx/react/utils/react/RawClickable'; +import { useDisableStateProps } from '@lumx/react/utils/disabled'; type HTMLButtonProps = DetailedHTMLProps, HTMLButtonElement>; @@ -107,18 +105,14 @@ export const ButtonRoot = forwardRef. - * If there is an href attribute, we display an instead of a + ); }); ButtonRoot.displayName = COMPONENT_NAME; diff --git a/packages/lumx-react/src/components/link/Link.test.tsx b/packages/lumx-react/src/components/link/Link.test.tsx index a26b73a3e..55218322d 100644 --- a/packages/lumx-react/src/components/link/Link.test.tsx +++ b/packages/lumx-react/src/components/link/Link.test.tsx @@ -85,9 +85,10 @@ describe(`<${Link.displayName}>`, () => { it('should render disabled link', async () => { const onClick = vi.fn(); const { link } = setup({ children: 'Label', isDisabled: true, href: 'https://example.com', onClick }); - // Disabled link do not exist so we fallback to a button - expect(screen.queryByRole('link')).not.toBeInTheDocument(); - expect(link).toHaveAttribute('disabled'); + expect(screen.queryByRole('link')).toBeInTheDocument(); + expect(link).toHaveAttribute('aria-disabled'); + // Simulate standard disabled state (not focusable) + expect(link).toHaveAttribute('tabindex', '-1'); await userEvent.click(link); expect(onClick).not.toHaveBeenCalled(); }); @@ -95,7 +96,9 @@ describe(`<${Link.displayName}>`, () => { it('should render aria-disabled button', async () => { const onClick = vi.fn(); const { link } = setup({ children: 'Label', 'aria-disabled': true, onClick }); - expect(link).toHaveAttribute('aria-disabled'); + expect(screen.queryByRole('button')).toBeInTheDocument(); + expect(link).toHaveAttribute('aria-disabled', 'true'); + expect(link).not.toHaveAttribute('tabindex'); await userEvent.click(link); expect(onClick).not.toHaveBeenCalled(); }); @@ -109,8 +112,7 @@ describe(`<${Link.displayName}>`, () => { onClick, }); expect(link).toHaveAccessibleName('Label'); - // Disabled link do not exist so we fallback to a button - expect(screen.queryByRole('link')).not.toBeInTheDocument(); + expect(screen.queryByRole('link')).toBeInTheDocument(); expect(link).toHaveAttribute('aria-disabled', 'true'); await userEvent.click(link); expect(onClick).not.toHaveBeenCalled(); diff --git a/packages/lumx-react/src/components/link/Link.tsx b/packages/lumx-react/src/components/link/Link.tsx index fa7b19728..abcb5c17b 100644 --- a/packages/lumx-react/src/components/link/Link.tsx +++ b/packages/lumx-react/src/components/link/Link.tsx @@ -12,8 +12,9 @@ import { } from '@lumx/core/js/utils/className'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; import { wrapChildrenIconWithSpaces } from '@lumx/react/utils/react/wrapChildrenIconWithSpaces'; -import { useDisableStateProps } from '@lumx/react/utils/disabled/useDisableStateProps'; import { HasAriaDisabled } from '@lumx/react/utils/type/HasAriaDisabled'; +import { RawClickable } from '@lumx/react/utils/react/RawClickable'; +import { useDisableStateProps } from '@lumx/react/utils/disabled'; type HTMLAnchorProps = React.DetailedHTMLProps, HTMLAnchorElement>; @@ -67,38 +68,26 @@ const CLASSNAME = getRootClassName(COMPONENT_NAME); * @return React element. */ export const Link = forwardRef((props, ref) => { - const { isAnyDisabled, disabledStateProps, otherProps } = useDisableStateProps(props); + const { disabledStateProps, otherProps } = useDisableStateProps(props); const { children, className, color: propColor, colorVariant: propColorVariant, - href, leftIcon, - linkAs, rightIcon, - target, typography, + linkAs, ...forwardedProps } = otherProps; const [color, colorVariant] = resolveColorWithVariants(propColor, propColorVariant); - const isLink = linkAs || href; - const Component = isLink && !isAnyDisabled ? linkAs || 'a' : 'button'; - const baseProps: React.ComponentProps = {}; - if (Component === 'button') { - baseProps.type = 'button'; - Object.assign(baseProps, disabledStateProps); - } else if (isLink) { - baseProps.href = href; - baseProps.target = target; - } - return ( - {rightIcon && } , )} - + ); }); Link.displayName = COMPONENT_NAME; diff --git a/packages/lumx-react/src/components/navigation/NavigationItem.tsx b/packages/lumx-react/src/components/navigation/NavigationItem.tsx index 7fc71defa..25f51973e 100644 --- a/packages/lumx-react/src/components/navigation/NavigationItem.tsx +++ b/packages/lumx-react/src/components/navigation/NavigationItem.tsx @@ -1,11 +1,12 @@ import React, { ElementType, ReactNode } from 'react'; import { Icon, Placement, Size, Tooltip, Text } from '@lumx/react'; import { getRootClassName, handleBasicClasses } from '@lumx/core/js/utils/className'; -import { ComponentRef, HasClassName, HasPolymorphicAs, HasTheme } from '@lumx/react/utils/type'; +import { ComponentRef, HasClassName, HasPolymorphicAs, HasRequiredLinkHref, HasTheme } from '@lumx/react/utils/type'; import classNames from 'classnames'; import { forwardRefPolymorphic } from '@lumx/react/utils/react/forwardRefPolymorphic'; import { useTheme } from '@lumx/react/utils/theme/ThemeContext'; import { useOverflowTooltipLabel } from '@lumx/react/hooks/useOverflowTooltipLabel'; +import { RawClickable } from '@lumx/react/utils/react/RawClickable'; type BaseNavigationItemProps = { /** Icon (SVG path). */ @@ -16,9 +17,6 @@ type BaseNavigationItemProps = { isCurrentPage?: boolean; }; -/** Make `href` required when `as` is `a` */ -type RequiredLinkHref = E extends 'a' ? { href: string } : Record; - /** * Navigation item props */ @@ -26,7 +24,7 @@ export type NavigationItemProps = HasPolymorphicAs< HasTheme & HasClassName & BaseNavigationItemProps & - RequiredLinkHref; + HasRequiredLinkHref; /** * Component display name. @@ -44,8 +42,6 @@ export const NavigationItem = Object.assign( const theme = useTheme(); const { tooltipLabel, labelRef } = useOverflowTooltipLabel(label); - const buttonProps = Element === 'button' ? { type: 'button' } : {}; - return (
  • - } aria-current={isCurrentPage ? 'page' : undefined} - {...buttonProps} {...forwardedProps} > {icon ? ( @@ -74,7 +70,7 @@ export const NavigationItem = Object.assign( {label} - +
  • ); diff --git a/packages/lumx-react/src/components/navigation/NavigationSection.tsx b/packages/lumx-react/src/components/navigation/NavigationSection.tsx index d7e2c5f14..3b4f7ff27 100644 --- a/packages/lumx-react/src/components/navigation/NavigationSection.tsx +++ b/packages/lumx-react/src/components/navigation/NavigationSection.tsx @@ -9,6 +9,7 @@ import { ThemeProvider, useTheme } from '@lumx/react/utils/theme/ThemeContext'; import { useId } from '@lumx/react/hooks/useId'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; +import { RawClickable } from '@lumx/react/utils/react/RawClickable'; import { CLASSNAME as ITEM_CLASSNAME } from './NavigationItem'; import { NavigationContext } from './context'; @@ -52,7 +53,8 @@ export const NavigationSection = forwardRef - + {isOpen && (isDropdown ? ( {shouldSplitActions ? (
    - {renderLink( - { - linkAs, - ...linkProps, - className: `${CLASSNAME}__link`, - onClick, - tabIndex: 0, - }, - icon && , - {label}, - )} + + {icon && } + {label} +
    ) : ( - renderButtonOrLink( - { - linkAs, - ...linkProps, - className: `${CLASSNAME}__link`, - tabIndex: 0, - onClick, - ...ariaProps, - }, - icon && , - {label}, - hasContent && ( + + {icon && } + {label} + {hasContent && ( - ), - ) + )} + )} {(closeMode === 'hide' || showChildren) && ( diff --git a/packages/lumx-react/src/components/thumbnail/Thumbnail.test.tsx b/packages/lumx-react/src/components/thumbnail/Thumbnail.test.tsx index 179a51fac..5c8beeb44 100644 --- a/packages/lumx-react/src/components/thumbnail/Thumbnail.test.tsx +++ b/packages/lumx-react/src/components/thumbnail/Thumbnail.test.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { DisabledStateProvider } from '@lumx/react/utils'; import { commonTestsSuiteRTL, SetupRenderOptions } from '@lumx/react/testing/utils'; import { queryByClassName } from '@lumx/react/testing/utils/queries'; -import { fireEvent, render } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import { Thumbnail, ThumbnailProps } from './Thumbnail'; const CLASSNAME = Thumbnail.className as string; @@ -28,8 +28,19 @@ describe(`<${Thumbnail.displayName}>`, () => { }, }); - describe('disabled state', () => { - it('should not be clickable when disabled from context', () => { + describe('clickable button', () => { + it('should render clickable button', async () => { + const onClick = vi.fn(); + const { thumbnail } = setup({ onClick, alt: 'Name' }); + const button = screen.getByRole('button', { name: 'Name' }); + expect(button).toBe(thumbnail); + expect(button).toHaveAttribute('type', 'button'); + + fireEvent.click(thumbnail as HTMLElement); + expect(onClick).toHaveBeenCalled(); + }); + + it('should not render button in disabled context', () => { const onClick = vi.fn(); const { thumbnail, container } = setup( { onClick, 'aria-label': 'thumbnail' }, @@ -47,8 +58,21 @@ describe(`<${Thumbnail.displayName}>`, () => { fireEvent.click(thumbnail as HTMLElement); expect(onClick).not.toHaveBeenCalled(); }); + }); + + describe('clickable link', () => { + it('should render clickable link', async () => { + const onClick = vi.fn((evt: any) => evt.preventDefault()); + const { thumbnail } = setup({ linkProps: { href: '#' }, onClick, alt: 'Name' }); + const link = screen.getByRole('link'); + expect(link).toBe(thumbnail); + expect(link).toHaveAttribute('href', '#'); + + fireEvent.click(thumbnail as HTMLElement); + expect(onClick).toHaveBeenCalled(); + }); - it('should have no href when disabled from context', () => { + it('should not render link in disabled context', () => { const { container, thumbnail } = setup( { linkAs: 'a', linkProps: { href: '#' }, 'aria-label': 'thumbnail' }, { diff --git a/packages/lumx-react/src/components/thumbnail/Thumbnail.tsx b/packages/lumx-react/src/components/thumbnail/Thumbnail.tsx index 86c5afa50..b1c98d3ef 100644 --- a/packages/lumx-react/src/components/thumbnail/Thumbnail.tsx +++ b/packages/lumx-react/src/components/thumbnail/Thumbnail.tsx @@ -22,6 +22,7 @@ import { useTheme } from '@lumx/react/utils/theme/ThemeContext'; import { forwardRef } from '@lumx/react/utils/react/forwardRef'; import { useDisableStateProps } from '@lumx/react/utils/disabled'; +import { RawClickable } from '@lumx/react/utils/react/RawClickable'; import { FocusPoint, ThumbnailSize, ThumbnailVariant } from './types'; type ImgHTMLProps = ImgHTMLAttributes; @@ -100,7 +101,7 @@ const DEFAULT_PROPS: Partial = { * @return React element. */ export const Thumbnail = forwardRef((props, ref) => { - const { isAnyDisabled, otherProps } = useDisableStateProps(props); + const { isAnyDisabled, otherProps, disabledStateProps } = useDisableStateProps(props); const defaultTheme = useTheme() || Theme.light; const { align, @@ -151,18 +152,17 @@ export const Thumbnail = forwardRef((props, ref) => { } const isLink = Boolean(linkProps?.href || linkAs); - const isButton = !!forwardedProps.onClick; - const isClickable = !isAnyDisabled && (isButton || isLink); + const isClickable = !isAnyDisabled && Boolean(isLink || !!forwardedProps.onClick); - let Wrapper: any = 'div'; + const Wrapper: any = isClickable ? RawClickable : 'div'; const wrapperProps = { ...forwardedProps }; - if (!isAnyDisabled && isLink) { - Wrapper = linkAs || 'a'; - Object.assign(wrapperProps, linkProps); - } else if (!isAnyDisabled && isButton) { - Wrapper = 'button'; - wrapperProps.type = forwardedProps.type || 'button'; - wrapperProps['aria-label'] = forwardedProps['aria-label'] || alt; + if (isClickable) { + Object.assign(wrapperProps, { as: linkAs || (linkProps?.href ? 'a' : 'button') }, disabledStateProps); + if (isLink) { + Object.assign(wrapperProps, linkProps); + } else { + wrapperProps['aria-label'] = forwardedProps['aria-label'] || alt; + } } // If we have a loading placeholder image that is really loaded (complete) diff --git a/packages/lumx-react/src/utils/react/RawClickable.test.tsx b/packages/lumx-react/src/utils/react/RawClickable.test.tsx new file mode 100644 index 000000000..6a1fa06f3 --- /dev/null +++ b/packages/lumx-react/src/utils/react/RawClickable.test.tsx @@ -0,0 +1,153 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { RawClickable, RawClickableProps } from './RawClickable'; +import { CustomLink } from '../../stories/utils/CustomLink'; + +/** + * Mounts the component and returns common DOM elements / data needed in multiple tests. + */ +const setup = (props: RawClickableProps) => { + render(); + const element = screen.getByTestId('raw-element'); + return { props, element }; +}; + +describe(``, () => { + describe('as a button', () => { + it('should render a button by default', () => { + const { element } = setup({ as: 'button', children: 'Click me' }); + expect(element.tagName).toBe('BUTTON'); + expect(element).toHaveAttribute('type', 'button'); + expect(screen.getByRole('button', { name: 'Click me' })).toBe(element); + }); + + it('should trigger onClick', async () => { + const onClick = vi.fn(); + const { element } = setup({ as: 'button', children: 'Click me', onClick }); + await userEvent.click(element); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('should be disabled with `disabled` prop', async () => { + const onClick = vi.fn(); + const { element } = setup({ as: 'button', children: 'Click me', onClick, disabled: true }); + expect(element).toBeDisabled(); + await userEvent.click(element); + expect(onClick).not.toHaveBeenCalled(); + }); + + it('should be disabled with `isDisabled` prop', async () => { + const onClick = vi.fn(); + const { element } = setup({ as: 'button', children: 'Click me', onClick, isDisabled: true }); + expect(element).toBeDisabled(); + await userEvent.click(element); + expect(onClick).not.toHaveBeenCalled(); + }); + + it('should be aria-disabled with `aria-disabled` prop', async () => { + const onClick = vi.fn(); + const { element } = setup({ as: 'button', children: 'Click me', onClick, 'aria-disabled': true }); + expect(element).not.toBeDisabled(); + expect(element).toHaveAttribute('aria-disabled', 'true'); + await userEvent.click(element); + expect(onClick).not.toHaveBeenCalled(); + }); + }); + + describe('as a link', () => { + const href = 'https://example.com'; + + it('should render a link with `href` prop', () => { + const { element } = setup({ as: 'a', children: 'Click me', href }); + expect(element.tagName).toBe('A'); + expect(element).toHaveAttribute('href', href); + expect(screen.getByRole('link', { name: 'Click me' })).toBe(element); + }); + + it('should trigger onClick', async () => { + const onClick = vi.fn((evt: any) => evt.preventDefault()); + const { element } = setup({ as: 'a', children: 'Click me', href, onClick }); + await userEvent.click(element); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('should be disabled with `disabled` prop', async () => { + const onClick = vi.fn(); + const { element } = setup({ as: 'a', children: 'Click me', href, onClick, disabled: true }); + expect(element).toHaveAttribute('aria-disabled', 'true'); + expect(element).toHaveAttribute('tabindex', '-1'); + await userEvent.click(element); + expect(onClick).not.toHaveBeenCalled(); + }); + + it('should be disabled with `isDisabled` prop', async () => { + const onClick = vi.fn(); + const { element } = setup({ as: 'a', children: 'Click me', href, onClick, isDisabled: true }); + expect(element).toHaveAttribute('aria-disabled', 'true'); + expect(element).toHaveAttribute('tabindex', '-1'); + await userEvent.click(element); + expect(onClick).not.toHaveBeenCalled(); + }); + + it('should be aria-disabled with `aria-disabled` prop', async () => { + const onClick = vi.fn(); + const { element } = setup({ as: 'a', children: 'Click me', href, onClick, 'aria-disabled': true }); + expect(element).toHaveAttribute('aria-disabled', 'true'); + await userEvent.click(element); + expect(onClick).not.toHaveBeenCalled(); + }); + }); + + describe('as a custom component', () => { + it('should render a custom component with `linkAs` prop', () => { + const { element } = setup({ as: CustomLink, children: 'Click me' }); + expect(element).toHaveAttribute('data-custom-link'); + }); + + it('should trigger onClick', async () => { + const onClick = vi.fn(); + const { element } = setup({ as: CustomLink, children: 'Click me', onClick }); + expect(element).toHaveAttribute('data-custom-link'); + await userEvent.click(element); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('should be disabled with `disabled` prop', async () => { + const onClick = vi.fn(); + const { element } = setup({ as: CustomLink, children: 'Click me', onClick, disabled: true }); + expect(element).toHaveAttribute('data-custom-link'); + expect(element).toHaveAttribute('aria-disabled', 'true'); + expect(element).toHaveAttribute('tabindex', '-1'); + await userEvent.click(element); + expect(onClick).not.toHaveBeenCalled(); + }); + }); + + describe('prop forwarding', () => { + it('should forward className', () => { + const { element } = setup({ as: 'button', className: 'foo bar' }); + expect(element).toHaveClass('foo bar'); + }); + + it('should forward ref and override type in button', () => { + const ref = React.createRef(); + const { element } = setup({ as: 'button', ref, type: 'submit' }); + expect(element).toHaveAttribute('type', 'submit'); + expect(ref.current).toBeInstanceOf(HTMLButtonElement); + }); + + it('should forward ref and override tabindex in link', () => { + const ref = React.createRef(); + const { element } = setup({ as: 'a', ref, href: '#', tabIndex: -1 }); + expect(ref.current).toBeInstanceOf(HTMLAnchorElement); + expect(element).toHaveAttribute('tabindex', '-1'); + }); + + it('should forward ref to custom component', () => { + const ref = React.createRef(); + setup({ as: CustomLink, ref }); + expect(ref.current).toBeInstanceOf(HTMLAnchorElement); + }); + }); +}); diff --git a/packages/lumx-react/src/utils/react/RawClickable.tsx b/packages/lumx-react/src/utils/react/RawClickable.tsx new file mode 100644 index 000000000..4edb900c7 --- /dev/null +++ b/packages/lumx-react/src/utils/react/RawClickable.tsx @@ -0,0 +1,65 @@ +import React, { AriaAttributes, ElementType } from 'react'; +import { forwardRefPolymorphic } from '@lumx/react/utils/react/forwardRefPolymorphic'; +import { ComponentRef, HasPolymorphicAs } from '@lumx/react/utils/type'; +import { HasRequiredLinkHref } from '@lumx/react/utils/type/HasRequiredLinkHref'; + +type ClickableElement = 'a' | 'button' | ElementType; + +type BaseClickableProps = { + children?: React.ReactNode; + isDisabled?: boolean; + disabled?: boolean; + 'aria-disabled'?: AriaAttributes['aria-disabled']; + onClick?: React.MouseEventHandler; +}; + +export type RawClickableProps = HasPolymorphicAs & + HasRequiredLinkHref & + BaseClickableProps; + +/** + * Render clickable element (link, button or custom element) + * (also does some basic disabled state handling) + */ +export const RawClickable = forwardRefPolymorphic( + (props: RawClickableProps, ref: ComponentRef) => { + const { + children, + onClick, + disabled, + isDisabled = disabled, + 'aria-disabled': ariaDisabled, + as, + ...forwardedProps + } = props; + + const isAnyDisabled = isDisabled || ariaDisabled === 'true' || ariaDisabled === true; + + const Component = as as any; + let clickableProps; + if (Component === 'button') { + clickableProps = { type: forwardedProps.type || 'button', disabled: isDisabled }; + } else { + clickableProps = { tabIndex: isDisabled ? '-1' : forwardedProps.tabIndex }; + } + + return ( + { + if (isAnyDisabled) { + event.stopPropagation(); + event.preventDefault(); + return; + } + onClick?.(event); + }} + > + {children} + + ); + }, +); diff --git a/packages/lumx-react/src/utils/react/renderButtonOrLink.tsx b/packages/lumx-react/src/utils/react/renderButtonOrLink.tsx deleted file mode 100644 index 3d3364563..000000000 --- a/packages/lumx-react/src/utils/react/renderButtonOrLink.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React, { ReactElement, ReactNode } from 'react'; -import { renderLink } from './renderLink'; - -interface Props { - linkAs?: any; - href?: any; -} - -/** - * Render