-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fixup! Fix(web-react): Mandatory href for anchors #DS-661
- Loading branch information
Showing
3 changed files
with
225 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
</> | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
</> | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; |