Skip to content

Commit

Permalink
fixup! Fix(web-react): Mandatory href for anchors #DS-661
Browse files Browse the repository at this point in the history
  • Loading branch information
literat committed Nov 20, 2024
1 parent 3dd8297 commit 4f86cc0
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 0 deletions.
98 changes: 98 additions & 0 deletions packages/web-react/src/components/Link/test-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/* eslint-disable react/no-unused-prop-types */
/* eslint-disable no-underscore-dangle */
/* eslint-disable react/require-default-props */
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-refresh/only-export-components */
/* eslint-disable jsx-a11y/anchor-is-valid */
import React, { ComponentPropsWithRef, ElementType, ForwardedRef, forwardRef } from 'react';
import { ActionColors } from '../../constants';
import { ActionLinkColorsDictionaryType, ChildrenProps, StyleProps, TransferProps } from '../../types';

type FixedForwardRef = <T, P = {}>(
render: (props: P, ref: React.Ref<T>) => React.ReactNode,
) => (props: P & React.RefAttributes<T>) => React.ReactNode;

const fixedForwardRef = forwardRef as FixedForwardRef;

const UNDERLINED_OPTIONS = {
ALWAYS: 'always',
HOVER: 'hover',
NEVER: 'never',
} as const;

type UnderlineOptions = (typeof UNDERLINED_OPTIONS)[keyof typeof UNDERLINED_OPTIONS];

type LinkProps<C = void> = {
/** Color of the Link */
color?: ActionLinkColorsDictionaryType<C>;
/** When is the Link underlined */
underlined?: UnderlineOptions;
/** Whether is the Link disabled */
isDisabled?: boolean;
};

type ElementProps<E extends ElementType = 'a'> = {
/**
* The HTML element or React element used to render the Link, e.g. 'a'.
*
* @default 'a'
*/
elementType?: E;
};

type LinkHrefProps<E extends ElementType> = E extends 'a'
? { href: string; elementType?: E; target?: '_blank'; title?: string }
: { href?: string; elementType: E };

type SpiritLinkProps<E extends ElementType = 'a', C = void> = LinkProps<C> &
ElementProps<E> & { ref?: ForwardedRef<ComponentPropsWithRef<E>['ref']> } & LinkHrefProps<E> &
ChildrenProps &
StyleProps &
TransferProps;

type LinkRef<E extends ElementType> = ComponentPropsWithRef<E>['ref'];

const defaultProps: Partial<SpiritLinkProps<ElementType>> = {
elementType: 'a',
color: ActionColors.PRIMARY,
hasVisitedStyleAllowed: false,
underlined: 'hover',
};

const _Link = <E extends ElementType = 'a'>(
props: SpiritLinkProps<E>,
ref: ForwardedRef<ComponentPropsWithRef<E>['ref']>,
) => {
const propsWithDefaults = { ...defaultProps, ...props };
const {
elementType: ElementTag = defaultProps.elementType as ElementType,
children,
...restProps
} = propsWithDefaults;

return (
<ElementTag
// {...otherProps}
// {...styleProps}
href={restProps.href}
// className={classNames(classProps, styleProps.className)}
ref={ref}
>
{children}
</ElementTag>
);
};

const Link = fixedForwardRef(_Link);

const LinkDefault = () => (
<>
<Link>Link</Link>

<Link href="https://www.example.com/" target="_blank" title="Warning">
⚠️ Link with Icon
</Link>

<Link elementType="button">Button</Link>
</>
);
70 changes: 70 additions & 0 deletions packages/web-react/src/components/Link/test-link2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/* eslint-disable react/no-unused-prop-types */
/* eslint-disable no-underscore-dangle */
/* eslint-disable react/require-default-props */
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-refresh/only-export-components */
/* eslint-disable jsx-a11y/anchor-is-valid */

import React, { ComponentPropsWithoutRef, ComponentPropsWithRef, ElementType, ForwardedRef, forwardRef } from 'react';

export const UNDERLINED_OPTIONS = {
ALWAYS: 'always',
HOVER: 'hover',
NEVER: 'never',
} as const;

type UnderlineOptions = (typeof UNDERLINED_OPTIONS)[keyof typeof UNDERLINED_OPTIONS];

// 1. Base props
interface BaseProps {
children?: React.ReactNode;
className?: string;
underlined?: UnderlineOptions;
isDisabled?: boolean;
}

// 2. Element specific props with strict href requirement
type ElementSpecificProps<E extends ElementType> = E extends 'a'
? {
elementType?: 'a';
href: string; // Required for anchor
target?: '_blank' | '_self' | '_parent' | '_top';
title?: string;
}
: {
elementType: Exclude<E, 'a'>; // Required for non-anchor
href?: never; // Not allowed for non-anchor
target?: never;
};

// 3. Combined props type
type SpiritLinkProps<E extends ElementType = 'a'> = BaseProps &
ElementSpecificProps<E> &
Omit<ComponentPropsWithoutRef<E>, keyof (BaseProps & ElementSpecificProps<E>)>;

// 4. Component implementation
const _Link = <E extends ElementType = 'a'>(
props: SpiritLinkProps<E>,
ref: ForwardedRef<ComponentPropsWithRef<E> extends { ref: infer R } ? R : never>,
) => {
const { elementType = 'a' as E, ...rest } = props;
const Element = elementType;

return <Element ref={ref} {...rest} />;
};

// 5. Typed forwardRef
const Link = forwardRef(_Link) as <E extends ElementType = 'a'>(
props: SpiritLinkProps<E> & { ref?: ForwardedRef<Element> },
) => JSX.Element;

const LinkDefault = () => (
<>
<Link href="/path">Valid</Link>
<Link elementType="button">Valid button</Link>
<Link>Invalid - missing href</Link>
<Link elementType="button" href="/path">
Invalid
</Link>
</>
);
57 changes: 57 additions & 0 deletions packages/web-react/src/components/Link/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { ElementType } from 'react';
import {
ActionLinkColorsDictionaryType,
ChildrenProps,
SpiritPolymorphicElementPropsWithRef,
StyleProps,
TransferProps,
} from '../../types/shared';

export const UNDERLINED_OPTIONS = {
ALWAYS: 'always',
HOVER: 'hover',
NEVER: 'never',
} as const;

export type AnchorTarget = '_blank' | '_self' | '_parent' | '_top';

export type UnderlineOptions = (typeof UNDERLINED_OPTIONS)[keyof typeof UNDERLINED_OPTIONS];

export interface LinkProps<C = void> {
/** Color of the Link */
color?: ActionLinkColorsDictionaryType<C>;
/** When is the Link underlined */
underlined?: UnderlineOptions;
/** Whether is the Link disabled */
isDisabled?: boolean;
/** Whether has the Link visited styles */
hasVisitedStyleAllowed?: boolean;
}

export type LinkElementTypeProps<E extends ElementType = 'a'> = {
/**
* The HTML element or React element used to render the Link, e.g. 'a'.
*
* @default 'a'
*/
elementType?: E;
};

type ElementSpecificProps<E extends ElementType> = E extends 'a'
? {
elementType?: E;
href: string;
target?: AnchorTarget;
}
: {
elementType: E;
href?: never;
target?: never;
};

export type SpiritLinkProps<E extends ElementType = 'a', C = void> = LinkProps<C> &
ChildrenProps &
StyleProps &
TransferProps &
LinkElementTypeProps<E> &
ElementSpecificProps<E>;

0 comments on commit 4f86cc0

Please sign in to comment.