Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions semcore/button/src/component/ButtonLink/ButtonLink.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createComponent, sstyled, Root, Component } from '@semcore/core';
import { createComponent, sstyled, Root, Component, type IRootComponentProps } from '@semcore/core';
import Link from '@semcore/link';
import React from 'react';

Expand Down Expand Up @@ -36,6 +36,12 @@ class RootButtonLink extends Component<
}
}

function Text(props: IRootComponentProps) {
const SText = Root;
const { styles } = props;
return sstyled(styles)(<SText render={Link.Text} />);
}

/**
* ButtonLink
*
Expand All @@ -44,7 +50,7 @@ class RootButtonLink extends Component<
export const ButtonLink = createComponent<ButtonLinkComponent, typeof RootButtonLink>(
RootButtonLink,
{
Text: Link.Text,
Text,
Addon: Link.Addon,
},
{
Expand Down
16 changes: 12 additions & 4 deletions semcore/button/src/component/ButtonLink/buttonLink.shadow.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,20 @@ SButtonLink {
}

SButtonLink[use='primary']:not([color]) {
color: var(--intergalactic-text-link, oklch(0.1 0.03 137 / 0.899)); /* TODO: --intergalactic-text-link-primary */
color: var(--intergalactic-text-link-primary, oklch(0.1 0.03 137 / 0.899));

SText {
text-decoration-color: transparent;
}

&[active],
&:active,
&:hover {
color: var(--intergalactic-text-link-hover-active, oklch(0.1 0.03 137 / 0.899)); /* TODO: --intergalactic-text-link-primary-hover-active */
color: var(--intergalactic-text-link-primary-hover-active, oklch(0.1 0.03 137 / 0.899));

SText {
text-decoration-color: currentColor;
}
}
}

Expand All @@ -19,11 +27,11 @@ SButtonLink[use='secondary']:not([color]) {
&[active],
&:active,
&:hover {
color: var(--intergalactic-text-hint-hover-active, oklch(0.086 0.026 145.8 / 0.605));
color: var(--intergalactic-text-link-primary, oklch(0.1 0.03 137 / 0.899));
}
}

SButtonLink[use='secondary'] SText {
text-decoration-style: dashed;
text-decoration-style: dotted;
text-decoration-color: currentColor;
}
1 change: 1 addition & 0 deletions semcore/data-table/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@semcore/checkbox": "^17.2.1",
"@semcore/spin": "^17.2.1",
"@semcore/tooltip": "^17.2.1",
"@semcore/link": "^17.2.1",
"@semcore/widget-empty": "^17.2.1"
},
"devDependencies": {
Expand Down
32 changes: 32 additions & 0 deletions semcore/data-table/src/components/LinkAction/LinkAction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ButtonLink } from '@semcore/button';
import Divider from '@semcore/divider';
import LinkExternalM from '@semcore/icon/LinkExternal/m';
import Link from '@semcore/link';
import type { NSText } from '@semcore/typography';
import React from 'react';

interface IProps extends NSText.BaseProps, NSText.EllipsisProps, NSText.HintProps {
displayHref: string;
externalHref: string;
internalAction: string | (() => void);
}

export class LinkAction extends React.PureComponent<IProps> {
render(): React.ReactNode {
const { displayHref, externalHref, internalAction, ...textProps } = this.props;
return (
<>
{typeof internalAction === 'string'
? (<Link href={internalAction}><Link.Text {...textProps}>{displayHref}</Link.Text></Link>)
: (<ButtonLink onClick={internalAction}><ButtonLink.Text {...textProps}>{displayHref}</ButtonLink.Text></ButtonLink>)}
<Divider orientation='vertical' mx={1} hMin={0} my={1} />
<Link
href={externalHref}
color='--gray-400'
addonLeft={() => <LinkExternalM width={14} height={14} />}
aria-label={externalHref}
/>
</>
);
}
}
2 changes: 2 additions & 0 deletions semcore/data-table/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
ColumnGroupConfig,
ColumnItemConfig,
} from './components/DataTable/DataTable.types';
import { LinkAction } from './components/LinkAction/LinkAction.tsx';
import { SelectableRows } from './store/SelectableRows';

const wrapDataTable = <PropsExtending extends {}>(
Expand All @@ -32,6 +33,7 @@ export {
ROW_GROUP,
wrapDataTable,
SelectableRows,
LinkAction,
};
export type {
DataTableSort,
Expand Down
49 changes: 40 additions & 9 deletions semcore/link/src/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,32 @@ import { Box, Hint } from '@semcore/base-components';
import type { IRootComponentProps } from '@semcore/core';
import { createComponent, Component, Root, sstyled, CORE_INSTANCE, INHERITED_NAME } from '@semcore/core';
import addonTextChildren from '@semcore/core/lib/utils/addonTextChildren';
import canUseDOM from '@semcore/core/lib/utils/canUseDOM';
import resolveColorEnhance from '@semcore/core/lib/utils/enhances/resolveColorEnhance';
import { findAllComponents } from '@semcore/core/lib/utils/findComponent';
import hasLabels from '@semcore/core/lib/utils/hasLabels';
import logger from '@semcore/core/lib/utils/logger';
import LinkExternalAltM from '@semcore/icon/LinkExternalAlt/m';
import type { NSText } from '@semcore/typography';
import { Text } from '@semcore/typography';
import React from 'react';

import type { LinkComponent, LinkProps } from './Link.types';
import type { NSLink } from './Link.types';
import style from './style/link.shadow.css';

type State = {
ariaLabelledByContent: string;
};

class RootLink extends Component<LinkProps, typeof RootLink.enhance, never, {}, State> {
class RootLink extends Component<NSLink.Props, typeof RootLink.enhance, never, {}, NSLink.State, NSLink.DefaultProps> {
static displayName = 'Link';

static style = style;
static enhance = [resolveColorEnhance()] as const;

static defaultProps = {
use: 'primary',
} as const;

containerRef = React.createRef<HTMLElement | null>();

state: State = {
state: NSLink.State = {
ariaLabelledByContent: '',
};

Expand Down Expand Up @@ -74,6 +77,26 @@ class RootLink extends Component<LinkProps, typeof RootLink.enhance, never, {},
};
}

private isExternalLink() {
const { children, href, isExternal } = this.asProps;

if (isExternal !== undefined) return isExternal;

const link = typeof children === 'string' && children.startsWith('http') ? children : href;

if (!link?.startsWith('http')) {
return false;
}

if (canUseDOM()) {
const linkUrl = new URL(link, window.location.origin);

return linkUrl.host !== window.location.host;
}

return false;
}

render() {
const {
styles,
Expand All @@ -96,6 +119,8 @@ class RootLink extends Component<LinkProps, typeof RootLink.enhance, never, {},
const hintContent = title ?? ariaLabel ?? this.state.ariaLabelledByContent ?? '';
const showHint = children === undefined || title;

const isExternal = this.isExternalLink();

const excludeProps = ['title', 'aria-disabled'];
if (!this.asProps['use:disabled']) {
excludeProps.push('disabled');
Expand All @@ -110,9 +135,11 @@ class RootLink extends Component<LinkProps, typeof RootLink.enhance, never, {},
render={Text}
text-color={resolveColor(color)}
tag='a'
target={isExternal ? '_blank' : undefined}
ref={this.containerRef}
__excludeProps={excludeProps}
aria-label={showHint ? hintContent : undefined}
isExternal={isExternal}
>
<SInner
tag='span'
Expand All @@ -125,7 +152,11 @@ class RootLink extends Component<LinkProps, typeof RootLink.enhance, never, {},
</Link.Addon>
)
: null}
{addonTextChildren(Children, Link.Text, Link.Addon)}
{
(this.isExternalLink() && Children.origin)
? (<Link.Text>{Children.origin}<LinkExternalAltM width={10} height={10} /></Link.Text>)
: addonTextChildren(Children, Link.Text, Link.Addon)
}
{AddonRight
? (
<Link.Addon>
Expand Down Expand Up @@ -166,7 +197,7 @@ function Addon(props: IRootComponentProps) {
*
* {@link https://developer.semrush.com/intergalactic/components/link/link-api/|API} | {@link https://developer.semrush.com/intergalactic/components/link/link-code/|Examples}
*/
const Link = createComponent<LinkComponent, typeof RootLink>(RootLink, {
const Link = createComponent<NSLink.Component, typeof RootLink>(RootLink, {
Text: LinkText,
Addon,
});
Expand Down
101 changes: 65 additions & 36 deletions semcore/link/src/Link.types.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,72 @@
import type { BoxProps, SimpleHintPopperProps } from '@semcore/base-components';
import type { Intergalactic, IRootComponentProps } from '@semcore/core';
import type { Intergalactic } from '@semcore/core';
import type Icon from '@semcore/icon';
import type { NSText } from '@semcore/typography';
import type React from 'react';

export type LinkProps = BoxProps & NSText.BaseProps & {
/**
* CSS property of the display link (inline|inline-block)
* @default false
* @deprecated. You should use default inline-flex for all cases.
*/
inline?: boolean;
/**
* Sets the link to the disabled state
*/
disabled?: boolean;
/**
* Sets the link to the active state
*/
active?: boolean;
/** This flag enables highlighting of the visited link
*/
enableVisited?: boolean;
/** The text will not be moved to a new line
* @default false
*/
noWrap?: boolean;
/** Left addon tag */
addonLeft?: typeof Icon | React.ElementType;
/** Right addon tag */
addonRight?: typeof Icon | React.ElementType;
/**
* The position of the popper relative to the trigger that called it.
* @default top
*/
hintPlacement?: SimpleHintPopperProps['placement'];
};
declare namespace NSLink {
type Use = 'primary' | 'secondary' | 'accent';
type Props = BoxProps & Intergalactic.InternalTypings.EfficientOmit<NSText.BaseProps, 'use'> & {
/**
* Type of Link.
*
* Primary. The vast majority of links. Tables with URLs and keywords, metrics in the summary.
*
* Secondary. Links such as 'Learn more', 'Show more', secondary information, opening hints.
*
* Accent. Use ONLY for action links within the texts. Limits, prompts indicating what to do, and options in the controls.
*
* @default 'primary'.
*/
use?: Use;
/**
* Sets the link to the disabled state
*/
disabled?: boolean;
/**
* Sets the link to the active state
*/
active?: boolean;
/** This flag enables highlighting of the visited link
*/
enableVisited?: boolean;
/** The text will not be moved to a new line
* @default false
*/
noWrap?: boolean;
/**
* The position of the popper relative to the trigger that called it.
* @default top
*/
hintPlacement?: SimpleHintPopperProps['placement'];
/** Left addon tag */
addonLeft?: typeof Icon | React.ElementType;
/** Right addon tag */
addonRight?: typeof Icon | React.ElementType;
/** Flag to mark a link as external. Use it in SSR. */
isExternal?: boolean;
};

type State = {
ariaLabelledByContent: string;
};

type DefaultProps = {
use: 'primary';
};

type Component = Intergalactic.Component<'a', Props> & {
Text: Intergalactic.Component<'span', NSText.Props>;
Addon: Intergalactic.Component<'span', BoxProps>;
};
}

/** @deprecated It will be removed in v18. */
export type LinkProps = NSLink.Props;

/** @deprecated It will be removed in v18. */
export type LinkComponent = NSLink.Component;

export type LinkComponent = Intergalactic.Component<'a', LinkProps> & {
Text: Intergalactic.Component<'span', NSText.Props>;
Addon: Intergalactic.Component<'span', BoxProps>;
export {
NSLink,
};
Loading
Loading