feat(css): global link classes for standalone and underline (#29298)
Issue number: Internal --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> N/A ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - Adds two global classes for `.ionic-link` (standalone) and `.ionic-link-underline` (underline) appearances. - The global classes apply when directly applied to an anchor element (`a`) or when used on a parent container that renders a link internally. **Usage** Developers will need to import the link global stylesheet at this time to leverage the following CSS classes. ```html <!-- Basic Usage --> <a href="#" class="ionic-link">Standalone</a> <a href="#" class="ionic-link-underline">Underline</a> <!-- Nested Usage --> <div class="ionic-link"> <a href="#">Standalone</a> </div> <div class="ionic-link-underline"> <a href="#">Underline</a> </div> ``` **Focus and Activated States** Developers should apply the `ion-focusable` and `ion-activatable` classes to the anchor elements to enable proper styling on a mobile device. For web-only usages, the fallback `:focus` and `:active` pseudo states will apply correctly. ```html <!-- Basic Usage --> <a href="#" class="ionic-link ion-focusable ion-activatable">Standalone</a> <a href="#" class="ionic-link-underline ion-focusable ion-activatable">Underline</a> ``` ### Design Changes This section is areas of the implementation that are not consistent with the design and why. 1. Font size: Link font sizing is inherited from its content. This is to provide visual consistency when using a link within a paragraph or existing text content. Links should not have an explicit font size, but can be customized by the developer if a specific font size is desired. 2. Color: `currentColor` was used in place of `$ionic-color-neutral-900`. This is to provide better visual consistency when a link is used within content. Text color should be set on the body or text content, which would apply neutral-900 in places where it is applied. 3. The text underline offset in the designs is ~3px, but all of the design implementation is on a 4px grid. We've landed on 2px for the offset here. Discussed with Design and these proposed changes were verbally approved. ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 4. Update the BREAKING.md file with the breaking change. 5. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/.github/CONTRIBUTING.md#footer for more information. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. -->
120
core/src/css/link.ionic.scss
Normal file
@ -0,0 +1,120 @@
|
||||
@use "../foundations/ionic.vars" as tokens;
|
||||
|
||||
// Link: Shared Styles (Standalone & Underline)
|
||||
// -------------------------------------------------------------------------------
|
||||
@mixin link-shared {
|
||||
display: inline-flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 4px;
|
||||
|
||||
transition: color 0.2s ease-in-out;
|
||||
|
||||
font-weight: 400;
|
||||
|
||||
text-decoration-color: inherit;
|
||||
|
||||
text-underline-offset: 2px;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
// Link: Visited
|
||||
// -------------------------------------------------------------------------------
|
||||
|
||||
&:visited {
|
||||
color: tokens.$ionic-color-info-500;
|
||||
}
|
||||
}
|
||||
|
||||
// Link: Standalone
|
||||
// -------------------------------------------------------------------------------
|
||||
|
||||
@mixin ionic-link {
|
||||
@include link-shared;
|
||||
|
||||
// Link: Standalone - Hover
|
||||
// -------------------------------------------------------------------------------
|
||||
|
||||
@media (any-hover: hover) {
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
color: tokens.$ionic-color-info-400;
|
||||
|
||||
text-decoration: none;
|
||||
|
||||
// Link: Standalone - Focus
|
||||
// -------------------------------------------------------------------------------
|
||||
|
||||
&:focus,
|
||||
&.ion-focused {
|
||||
outline: 2px solid tokens.$ionic-color-primary-100;
|
||||
outline-offset: 2px;
|
||||
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
// Link: Standalone - Active
|
||||
// -------------------------------------------------------------------------------
|
||||
|
||||
&:active,
|
||||
&.ion-activated {
|
||||
color: tokens.$ionic-color-info-500;
|
||||
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
a.ionic-link,
|
||||
:not(a).ionic-link a {
|
||||
@include ionic-link;
|
||||
}
|
||||
|
||||
// Link: Underline
|
||||
// -------------------------------------------------------------------------------
|
||||
|
||||
@mixin ionic-link-underline {
|
||||
@include link-shared;
|
||||
|
||||
// Link: Underline - Hover
|
||||
// -------------------------------------------------------------------------------
|
||||
|
||||
@media (any-hover: hover) {
|
||||
&:hover {
|
||||
color: tokens.$ionic-color-info-400;
|
||||
}
|
||||
}
|
||||
|
||||
color: currentColor;
|
||||
|
||||
text-decoration: underline;
|
||||
|
||||
// Link: Underline - Focus
|
||||
// -------------------------------------------------------------------------------
|
||||
|
||||
&:focus,
|
||||
&.ion-focused {
|
||||
outline: 2px solid tokens.$ionic-color-primary-100;
|
||||
outline-offset: 2px;
|
||||
|
||||
color: currentColor;
|
||||
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
// Link: Underline - Active
|
||||
// -------------------------------------------------------------------------------
|
||||
|
||||
&:active,
|
||||
&.ion-activated {
|
||||
color: tokens.$ionic-color-info-500;
|
||||
}
|
||||
}
|
||||
|
||||
a.ionic-link-underline,
|
||||
:not(a).ionic-link-underline a {
|
||||
@include ionic-link-underline;
|
||||
}
|
77
core/src/css/test/link/basic/index.html
Normal file
@ -0,0 +1,77 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Link - Basic</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../css/link.ionic.css" rel="stylesheet" />
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
<style>
|
||||
.links div {
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Link - Basic</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<h3>Standalone (.ionic-link)</h3>
|
||||
<div class="links" id="standalone">
|
||||
<div><a class="ionic-link" href="#">Link - Default</a></div>
|
||||
<div><a class="ionic-link ion-activated" href="#">Link - Active</a></div>
|
||||
<div><a class="ionic-link ion-focused" href="#">Link - Focused</a></div>
|
||||
<div><a class="ionic-link" href="">Link - Visited</a></div>
|
||||
<div>
|
||||
<a class="ionic-link" href="#">Link with Icon <ion-icon name="open-outline"></ion-icon></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Underline (.ionic-link-underline)</h3>
|
||||
<div class="links" id="underline">
|
||||
<div><a class="ionic-link-underline" href="#">Underline - Default</a></div>
|
||||
<div><a class="ionic-link-underline ion-activated" href="#">Underline - Active</a></div>
|
||||
<div><a class="ionic-link-underline ion-focused" href="#">Underline - Focused</a></div>
|
||||
<div><a class="ionic-link-underline" href="">Underline - Visited</a></div>
|
||||
<div>
|
||||
<a class="ionic-link-underline" href="#">Link with Icon <ion-icon name="open-outline"></ion-icon></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Links in Content</h3>
|
||||
<div class="links" id="links-in-content">
|
||||
<div>
|
||||
Lorem ipsum dolor sit amet consectetur, <a class="ionic-link" href="#">default link</a> adipisicing elit.
|
||||
</div>
|
||||
<div>
|
||||
Lorem ipsum dolor sit amet consectetur,
|
||||
<a class="ionic-link-underline" href="#">underline link</a> adipisicing elit.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Parent Element</h3>
|
||||
<div class="links">
|
||||
<div class="ionic-link" id="standalone-nested">
|
||||
<p>Lorem ipsum dolor sit amet consectetur, <a href="#">default link</a> adipisicing elit.</p>
|
||||
</div>
|
||||
<div class="ionic-link-underline" id="underline-nested">
|
||||
<p>Lorem ipsum dolor sit amet consectetur, <a href="#">underline link</a> adipisicing elit.</p>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
41
core/src/css/test/link/basic/link.e2e.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* Link global classes are only available in the Ionic theme.
|
||||
*/
|
||||
configs({ modes: ['ionic-md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('link global classes'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/src/css/test/link/basic/index.html', config);
|
||||
});
|
||||
|
||||
test.describe('.ion-link class', () => {
|
||||
test('should apply to anchor elements', async ({ page }) => {
|
||||
const standalone = page.locator('#standalone');
|
||||
|
||||
await expect(standalone).toHaveScreenshot(screenshot('link-standalone'));
|
||||
});
|
||||
|
||||
test('should apply to child anchor elements', async ({ page }) => {
|
||||
const standalone = page.locator('#standalone-nested');
|
||||
|
||||
await expect(standalone).toHaveScreenshot(screenshot('link-standalone-nested'));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('.ion-link-underline class', () => {
|
||||
test('should apply to anchor elements', async ({ page }) => {
|
||||
const standalone = page.locator('#underline');
|
||||
|
||||
await expect(standalone).toHaveScreenshot(screenshot('link-underline'));
|
||||
});
|
||||
|
||||
test('should apply to child anchor elements', async ({ page }) => {
|
||||
const standalone = page.locator('#underline-nested');
|
||||
|
||||
await expect(standalone).toHaveScreenshot(screenshot('link-underline-nested'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 8.3 KiB |
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 6.6 KiB |
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 9.7 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 9.3 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 4.6 KiB |