@spaced-out/ui-design-system
Version:
Sense UI components library
232 lines (210 loc) • 6.5 kB
Flow
// @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>
);
},
);