UNPKG

@spaced-out/ui-design-system

Version:
232 lines (210 loc) 6.5 kB
// @flow strict import * as React from 'react'; import type {ColorTypes} from '../../types/typography'; import {TEXT_COLORS} from '../../types/typography'; import classify from '../../utils/classify'; import {ConditionalWrapper} from '../ConditionalWrapper'; import type {IconSize, IconType} from '../Icon'; import {Icon, ICON_SIZE} from '../Icon'; import css from '../../styles/typography.module.css'; export const LINK_AS = Object.freeze({ bodyLarge: 'bodyLarge', bodyMedium: 'bodyMedium', bodySmall: 'bodySmall', buttonTextExtraSmall: 'buttonTextExtraSmall', buttonTextMedium: 'buttonTextMedium', buttonTextSmall: 'buttonTextSmall', formInputMedium: 'formInputMedium', formInputSmall: 'formInputSmall', formLabelMedium: 'formLabelMedium', formLabelSmall: 'formLabelSmall', jumboMedium: 'jumboMedium', subTitleExtraSmall: 'subTitleExtraSmall', subTitleLarge: 'subTitleLarge', subTitleMedium: 'subTitleMedium', subTitleSmall: 'subTitleSmall', titleMedium: 'titleMedium', }); export type LinkAs = $Values<typeof LINK_AS>; export const ANCHOR_REL = Object.freeze({ alternate: 'alternate', author: 'author', bookmark: 'bookmark', external: 'external', help: 'help', license: 'license', next: 'next', nofollow: 'nofollow', noopener: 'noopener', noreferrer: 'noreferrer', search: 'search', tag: 'tag', }); export type AnchorRel = $Values<typeof ANCHOR_REL>; export const ANCHOR_TARGET = Object.freeze({ _blank: '_blank', _self: '_self', _parent: '_parent', _top: '_top', framename: 'framename', }); export type AnchorTarget = $Values<typeof ANCHOR_TARGET>; export type BaseLinkProps = { children: React.Node, onClick?: ?(SyntheticEvent<HTMLElement>) => mixed, tabIndex?: number, disabled?: boolean, className?: string, as?: LinkAs, rel?: AnchorRel, target?: AnchorTarget, iconLeftName?: string, iconLeftSize?: IconSize, iconLeftType?: IconType, iconRightName?: string, iconRightSize?: IconSize, iconRightType?: IconType, /** * IMPORTANT: If you are using `to` make sure to provide link component from your router * if you want to prevent full page reloads in a Single Page Application (SPA). * * Using `href` in anchor tags causes the browser to navigate to a new URL, * resulting in a full page reload. However, in a Single Page Application (SPA), we aim to provide a seamless * user experience without such reloads. * * To achieve client-side navigation and prevent page reloads, use client-side routing libraries * (e.g., React Router) and their navigation components (e.g., <Link> or <a> with an onClick handler) * to handle navigation within your SPA. These components work without triggering full page reloads * and maintain the SPA's performance and user experience. * */ to?: string, href?: string, ... }; export type LinkProps = { ...BaseLinkProps, color?: ColorTypes, underline?: boolean, /** * Provide your router's link component * * import {Link} from 'src/rerouter'; * import {Link as GenesisLink} from '@spaced-out/ui-design-system/lib/components/Link'; * * <GenesisLink linkComponent={Link} to="/pages" /> */ linkComponent?: React.AbstractComponent<BaseLinkProps, ?HTMLAnchorElement>, ... }; export const Link: React$AbstractComponent<LinkProps, ?HTMLAnchorElement> = React.forwardRef<LinkProps, ?HTMLAnchorElement>( ( { color = TEXT_COLORS.clickable, children, className, as = 'buttonTextExtraSmall', underline = true, tabIndex = 0, disabled, onClick, linkComponent: LinkComponent = DefaultLink, iconLeftName, iconLeftSize = ICON_SIZE.small, iconLeftType, iconRightName, iconRightSize = ICON_SIZE.small, iconRightType, ...props }: LinkProps, ref, ) => { const linkRef = React.useRef(null); React.useImperativeHandle(ref, () => linkRef.current); React.useEffect(() => { if (disabled) { linkRef.current?.blur(); } }, [disabled]); const handleClick = (event: SyntheticEvent<HTMLElement>) => { if (disabled) { event.preventDefault(); return; } onClick?.(event); }; /** * By spec anchor tag wont call onClick on enter key press when the element is focussed * as a workaround we would need to listen to key press event and call onClick * manually, one workaround to avoid this is to have empty href along with onClick * but that would break accessibility */ const handleKeyPress = (event) => { if (event.key === 'Enter' && onClick) { handleClick(event); } }; return ( <LinkComponent {...props} tabIndex={disabled ? -1 : tabIndex} ref={linkRef} data-testid="Link" className={classify( css.link, css[as], css[color], { [css.underline]: underline && !(iconLeftName || iconRightName), [css.disabled]: disabled, }, className, )} onClick={handleClick} onKeyPress={handleKeyPress} > {!!iconLeftName && ( <Icon name={iconLeftName} size={iconLeftSize} type={iconLeftType} className={classify(css[color], {[css.disabled]: disabled})} /> )} <ConditionalWrapper condition={Boolean(iconLeftName || iconRightName)} wrapper={(children) => ( <span className={classify({ [css.underline]: underline, })} > {children} </span> )} > {children} </ConditionalWrapper> {!!iconRightName && ( <Icon name={iconRightName} size={iconRightSize} type={iconRightType} className={css[color]} /> )} </LinkComponent> ); }, ); const DefaultLink = React.forwardRef<BaseLinkProps, HTMLAnchorElement>( ({children, href, to, ...props}, ref) => { const resolvedHref = to ?? href; return ( <a {...props} href={resolvedHref} ref={ref}> {children} </a> ); }, );