Skip to content

Commit

Permalink
feat(Link): add props – before, after, noUnderline (#7957)
Browse files Browse the repository at this point in the history
h2. Описание

Решение из #7579 не подошло из-за того, что подчеркивания распространяется и на пробел.

_До #7579_
<img width="188" alt="image" src="https://github.com/user-attachments/assets/efa9f083-dab6-4403-b49b-c14cb3d77f14">

_После #7579_
<img width="188" alt="image" src="https://github.com/user-attachments/assets/e7337004-691c-49c9-8893-aef8612643f4">

По аналогии с другими компонентами, самое простое оказалось добавить `before` / `after` с указанием отступов через соответствующие CSS классы, что избавляет от каскада `.host :global(.vkuiIcon)`.

Заодно добавил свойство `noUnderline`.

<details><summary>Нюансы про альтернативное решение</summary>
<p>

Решение через `.host :global(.vkuiIcon:first-child)` и `.host :global(.vkuiIcon:last-child)` не работает без оборачивания текстового блока в DOM-элемент. Здесь без просьбы пользователя оборачивать в `<span>` никак.

</p>
</details> 

h2. Release notes
h2. BREAKING CHANGE
- Link: теперь для передачи иконок следует использовать параметры `before` и `after`, CSS свойства, которые через каскад задавались переданным иконкам в `children`, удалены
  <details>
  <summary>Пример миграции</summary>
  
  ```diff
  <Link
    href="#"
  + after={<Icon12Example />}
  >
    Текст ссылки
  - <Icon12Example />
  </Link>
  ```
  </details>

h2. Улучшения
- Link: появился параметр `noUnderline`, отключающий подчеркивание при наведении
  • Loading branch information
inomdzhon authored Nov 20, 2024
1 parent 9ada42b commit 77fe44d
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 47 deletions.
36 changes: 32 additions & 4 deletions packages/vkui/src/components/Link/Link.e2e-playground.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Icon24ExternalLinkOutline } from '@vkontakte/icons';
import { Icon16ChainOutline, Icon24ExternalLinkOutline } from '@vkontakte/icons';
import { ComponentPlayground, type ComponentPlaygroundProps } from '@vkui-e2e/playground-helpers';
import { Link, type LinkProps } from './Link';

Expand All @@ -7,12 +7,40 @@ export const LinkFocusVisiblePlayground = (props: ComponentPlaygroundProps) => (
{(props: LinkProps) => (
<div style={{ width: 300, padding: 10 }}>
Нажимая «Продолжить», вы принимаете{' '}
<Link href="#" {...props}>
пользовательское соглашение&nbsp;
<Icon24ExternalLinkOutline width={16} height={16} />
<Link href="#" after={<Icon24ExternalLinkOutline width={16} height={16} />} {...props}>
пользовательское соглашение
</Link>
...
</div>
)}
</ComponentPlayground>
);

export const LinkWithIcons = (props: ComponentPlaygroundProps) => (
<ComponentPlayground {...props}>
{() => (
<>
<Link
href="https://google.com"
target="_blank"
after={<Icon24ExternalLinkOutline width={16} height={16} />}
>
https://google.com
</Link>
<br />
<Link href="/" before={<Icon16ChainOutline />}>
Главная
</Link>
<br />
<Link
href="https://vk.com/video807566_169118280"
target="_blank"
before={<Icon16ChainOutline />}
after={<Icon24ExternalLinkOutline width={16} height={16} />}
>
Главная в новом окне
</Link>
</>
)}
</ComponentPlayground>
);
11 changes: 10 additions & 1 deletion packages/vkui/src/components/Link/Link.e2e.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test } from '@vkui-e2e/test';
import { Platform } from '../../lib/platform';
import { LinkFocusVisiblePlayground } from './Link.e2e-playground';
import { LinkFocusVisiblePlayground, LinkWithIcons } from './Link.e2e-playground';

test.describe('Link', () => {
test.use({
Expand All @@ -19,4 +19,13 @@ test.describe('Link', () => {
await page.keyboard.press('Tab');
await expectScreenshotClippedToContent();
});

test('icon gaps', async ({
mount,
expectScreenshotClippedToContent,
componentPlaygroundProps,
}) => {
await mount(<LinkWithIcons {...componentPlaygroundProps} />);
await expectScreenshotClippedToContent();
});
});
14 changes: 11 additions & 3 deletions packages/vkui/src/components/Link/Link.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
}

@media (--hover-has) {
.host:hover {
.withUnderline:hover {
text-decoration: underline;
}
}
Expand All @@ -23,8 +23,16 @@
color: var(--vkui--color_text_link_visited);
}

/* stylelint-disable-next-line selector-pseudo-class-disallowed-list */
.host :global(.vkuiIcon) {
.before,
.after {
display: inline-block;
vertical-align: middle;
}

.before {
margin-inline-end: var(--vkui--spacing_size_xs);
}

.after {
margin-inline-start: var(--vkui--spacing_size_xs);
}
33 changes: 24 additions & 9 deletions packages/vkui/src/components/Link/Link.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Icon24ExternalLinkOutline } from '@vkontakte/icons';
import { Icon16ChainOutline, Icon24ExternalLinkOutline } from '@vkontakte/icons';
import { CanvasFullLayout, DisableCartesianParam } from '../../storybook/constants';
import { Link, type LinkProps } from './Link';

Expand All @@ -21,15 +21,30 @@ export const Playground: Story = {
};

export const WithIcon: Story = {
...Playground,
args: {
href: 'https://google.com',
target: '_blank',
children: (
render: function Render() {
return (
<>
https://google.com&nbsp;
<Icon24ExternalLinkOutline width={16} height={16} />
<Link
href="https://google.com"
target="_blank"
after={<Icon24ExternalLinkOutline width={16} height={16} />}
>
https://google.com
</Link>
<br />
<Link href="/" before={<Icon16ChainOutline />}>
Главная
</Link>
<br />
<Link
href="https://vk.com/video807566_169118280"
target="_blank"
before={<Icon16ChainOutline />}
after={<Icon24ExternalLinkOutline width={16} height={16} />}
>
Главная в новом окне
</Link>
</>
),
);
},
};
56 changes: 37 additions & 19 deletions packages/vkui/src/components/Link/Link.test.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,51 @@
import { render, screen } from '@testing-library/react';
import { render } from '@testing-library/react';
import { baselineComponent } from '../../testing/utils';
import { Link, type LinkProps } from './Link';
import { Link } from './Link';
import styles from './Link.module.css';

const LinkTest = (props: LinkProps) => (
<Link data-testid="link" {...props}>
Link
</Link>
);
const link = () => screen.getByTestId('link');

describe('Link', () => {
describe(Link, () => {
baselineComponent((props) => (
<Link href="https://vk.com" {...props}>
Link
</Link>
));

it('Component: default Link is a button', () => {
render(<LinkTest />);
expect(link().tagName.toLowerCase()).toMatch('button');
it('should use <button> tag', () => {
const result = render(<Link />);
expect(result.getByRole('button')).toBeInTheDocument();
});

it('should use <a> tag', () => {
const result = render(<Link href="https://vk.com" />);
expect(result.getByRole('link')).toBeInTheDocument();
});

it('should render before and after elements', () => {
const result = render(
<Link
href="https://vk.com"
before={<span data-testid="before" />}
after={<span data-testid="after" />}
/>,
);

expect(result.getByTestId('before')).toBeInTheDocument();
expect(result.getByTestId('after')).toBeInTheDocument();
});

it('Component: Link w/ href is a link', () => {
render(<LinkTest href="https://vk.com" />);
expect(link().tagName.toLowerCase()).toMatch('a');
it('should disable underline', () => {
const result = render(<Link href="https://vk.com" />);
expect(result.getByRole('link')).toHaveClass(styles.withUnderline);

result.rerender(<Link href="https://vk.com" noUnderline />);
expect(result.getByRole('link')).not.toHaveClass(styles.withUnderline);
});

it('Component: Link w/ Component and href is [Component]', () => {
render(<LinkTest href="https://vk.com" Component="div" />);
expect(link().tagName.toLowerCase()).toMatch('div');
it('should use visited style', () => {
const result = render(<Link href="https://vk.com" />);
expect(result.getByRole('link')).not.toHaveClass(styles.hasVisited);

result.rerender(<Link href="https://vk.com" hasVisited />);
expect(result.getByRole('link')).toHaveClass(styles.hasVisited);
});
});
30 changes: 28 additions & 2 deletions packages/vkui/src/components/Link/Link.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import type { ReactElement } from 'react';
import { classNames } from '@vkontakte/vkjs';
import { Tappable, type TappableProps } from '../Tappable/Tappable';
import styles from './Link.module.css';

export interface LinkProps extends TappableProps {
/**
* Включает состояние `visited`, которое позволяет пользователю понять посещал ли он ссылку или нет
* Иконка слева.
*/
before?: ReactElement;
/**
* Иконка справа.
*/
after?: ReactElement;
/**
* Выключает появления нижнего подчеркивания при наведении.
*/
noUnderline?: boolean;
/**
* Включает состояние `visited`, которое позволяет пользователю понять посещал ли он ссылку или нет.
*/
hasVisited?: boolean;
}
Expand All @@ -13,22 +26,35 @@ export interface LinkProps extends TappableProps {
* @see https://vkcom.github.io/VKUI/#/Link
*/
export const Link = ({
before: beforeProp,
after: afterProp,
noUnderline,
hasVisited,
children,
className,
...restProps
}: LinkProps): React.ReactNode => {
const before = beforeProp ? <span className={styles.before}>{beforeProp}</span> : null;
const after = afterProp ? <span className={styles.after}>{afterProp}</span> : null;

return (
<Tappable
Component={restProps.href ? 'a' : 'button'}
{...restProps}
className={classNames(styles.host, hasVisited && styles.hasVisited, className)}
className={classNames(
styles.host,
hasVisited && styles.hasVisited,
noUnderline ? undefined : styles.withUnderline,
className,
)}
hasHover={false}
activeMode="opacity"
hoverMode="none"
focusVisibleMode="outside"
>
{before}
{children}
{after}
</Tappable>
);
};
28 changes: 19 additions & 9 deletions packages/vkui/src/components/Link/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,23 @@
- вес шрифта

```jsx { "props": { "layout": false, "iframe": false } }
<div
style={{
padding: 24,
}}
>
<div style={{ padding: 24 }}>
<Link href="#/About">О VKUI</Link>

<Spacing size={24} />

<Link href="https://google.com" target="_blank">
https://google.com&nbsp;
<Icon24ExternalLinkOutline width={16} height={16} />
<Link
href="https://google.com"
target="_blank"
after={<Icon24ExternalLinkOutline width={16} height={16} />}
>
https://google.com
</Link>

<Spacing size={24} />

<div style={{ width: 304 }}>
Нажимая «Продолжить», вы принимаете&nbsp;<Link href="#">пользовательское соглашение</Link> и{' '}
Нажимая «Продолжить», вы принимаете <Link href="#">пользовательское соглашение</Link> и{' '}
<Link href="#">политику конфиденциальности</Link>.
</div>

Expand All @@ -36,5 +35,16 @@
Если посетить эту ссылку, то она будет использовать состояние
<code>visited</code>
</Link>

<Spacing size={24} />

<Link
href="https://vk.com/video807566_169118280"
target="_blank"
before={<Icon16ChainOutline />}
after={<Icon24ExternalLinkOutline width={16} height={16} />}
>
Главная в новом окне
</Link>
</div>
```
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 77fe44d

Please sign in to comment.