Skip to content
Merged
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
17 changes: 16 additions & 1 deletion BREAKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,8 @@ The following breaking changes apply to `ion-router-link`:
2. `--background` has been removed. <sup>[2](#version-10x-router-link-removed-css-variables)</sup>
3. The color applied by the `color` prop is now driven by the centralized Ionic Theming system, scoped to the new `hue` property. <sup>[3](#version-10x-router-link-hue)</sup>
4. The text color now uses the color's `foreground` variant instead of `base`. <sup>[4](#version-10x-router-link-foreground)</sup>
5. Mode classes (`ion-router-link.md`, `ion-router-link.ios`) are no longer supported. <sup>[5](#version-10x-router-link-mode-classes)</sup>
5. In the `md` and `ios` themes, a focused router link now shows a tinted background instead of the browser's focus ring. In the `ionic` theme, it shows a 2px ring. <sup>[5](#version-10x-router-link-focus)</sup>
6. Mode classes (`ion-router-link.md`, `ion-router-link.ios`) are no longer supported. <sup>[6](#version-10x-router-link-mode-classes)</sup>

<h5 id="version-10x-router-link-replaced-css-variables">Replaced CSS variables</h5>

Expand Down Expand Up @@ -528,6 +529,20 @@ A bold router link now uses the `foreground` variant of its color instead of `ba
| Default | `IonRouterLink.hue.bold.default.color` | `var(--ion-color-primary)` | `--ion-router-link-hue-bold-default-color` |
| `color` set | `IonRouterLink.hue.bold.semantic.default.color` | `var(--ion-color-base)` | `--ion-router-link-hue-bold-semantic-default-color` |

<h5 id="version-10x-router-link-focus">Focus style</h5>

When a router link is focused with the keyboard in the `md` and `ios` themes, it now shows a background tinted with its color instead of the browser's default focus ring, the same as a clear button: 12% opacity in `md` and 10% in `ios`. The background reaches 2px above and below the text and 4px to each side, without moving it. In the `ionic` theme, it shows a 2px ring with no background. Customize it with the tokens for global styles, or the CSS variables for component-specific styles:

| Property | Token (global) | CSS variable (component-specific) |
|---|---|---|
| Background | `IonRouterLink.hue.bold.focus.background` | `--ion-router-link-hue-bold-focus-background` |
| Background, `color` set | `IonRouterLink.hue.bold.semantic.focus.background` | `--ion-router-link-hue-bold-semantic-focus-background` |
| Corner radius | `IonRouterLink.state.focus.border.radius` | `--ion-router-link-state-focus-border-radius` |
| Padding | `IonRouterLink.state.focus.padding` | `--ion-router-link-state-focus-padding-top`, `-end`, `-bottom`, `-start` |

> [!NOTE]
> A router link with `hue="subtle"` reads the `subtle` tokens instead. To show a ring, set `IonRouterLink.state.focus.ring.color`, `.style` and `.width`.

<h5 id="version-10x-router-link-mode-classes">Mode classes</h5>

Remove any instances that target the mode classes: `ion-router-link.md`, `ion-router-link.ios`.
Expand Down
24 changes: 24 additions & 0 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2241,10 +2241,34 @@ ion-router-link,prop,rel,string | undefined,undefined,false,false
ion-router-link,prop,routerAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
ion-router-link,prop,routerDirection,"back" | "forward" | "root",'forward',false,false
ion-router-link,prop,target,string | undefined,undefined,false,false
ion-router-link,css-prop,--ion-router-link-hue-bold-activated-color
ion-router-link,css-prop,--ion-router-link-hue-bold-default-color
ion-router-link,css-prop,--ion-router-link-hue-bold-focus-background
ion-router-link,css-prop,--ion-router-link-hue-bold-semantic-activated-color
ion-router-link,css-prop,--ion-router-link-hue-bold-semantic-default-color
ion-router-link,css-prop,--ion-router-link-hue-bold-semantic-focus-background
ion-router-link,css-prop,--ion-router-link-hue-bold-semantic-visited-color
ion-router-link,css-prop,--ion-router-link-hue-bold-visited-color
ion-router-link,css-prop,--ion-router-link-hue-subtle-activated-color
ion-router-link,css-prop,--ion-router-link-hue-subtle-default-color
ion-router-link,css-prop,--ion-router-link-hue-subtle-focus-background
ion-router-link,css-prop,--ion-router-link-hue-subtle-semantic-activated-color
ion-router-link,css-prop,--ion-router-link-hue-subtle-semantic-default-color
ion-router-link,css-prop,--ion-router-link-hue-subtle-semantic-focus-background
ion-router-link,css-prop,--ion-router-link-hue-subtle-semantic-visited-color
ion-router-link,css-prop,--ion-router-link-hue-subtle-visited-color
ion-router-link,css-prop,--ion-router-link-state-activated-text-decoration
ion-router-link,css-prop,--ion-router-link-state-focus-border-radius
ion-router-link,css-prop,--ion-router-link-state-focus-padding-bottom
ion-router-link,css-prop,--ion-router-link-state-focus-padding-end
ion-router-link,css-prop,--ion-router-link-state-focus-padding-start
ion-router-link,css-prop,--ion-router-link-state-focus-padding-top
ion-router-link,css-prop,--ion-router-link-state-focus-ring-color
ion-router-link,css-prop,--ion-router-link-state-focus-ring-offset
ion-router-link,css-prop,--ion-router-link-state-focus-ring-style
ion-router-link,css-prop,--ion-router-link-state-focus-ring-width
ion-router-link,css-prop,--ion-router-link-state-focus-text-decoration
ion-router-link,css-prop,--ion-router-link-text-decoration

ion-router-outlet,shadow
ion-router-outlet,prop,animated,boolean,true,false,false
Expand Down
24 changes: 24 additions & 0 deletions core/src/components/router-link/_router-link.docs.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,30 @@
/**
* @prop --ion-router-link-hue-bold-default-color: Text color of the `bold` hue when no semantic color is applied
* @prop --ion-router-link-hue-bold-visited-color: Text color of the `bold` hue when no semantic color is applied and the link has been visited
* @prop --ion-router-link-hue-bold-activated-color: Text color of the `bold` hue when no semantic color is applied and the link is pressed
* @prop --ion-router-link-hue-bold-focus-background: Background of the `bold` hue when no semantic color is applied and the link is focused
* @prop --ion-router-link-hue-bold-semantic-default-color: Text color of the `bold` hue when a semantic color is applied
* @prop --ion-router-link-hue-bold-semantic-visited-color: Text color of the `bold` hue when a semantic color is applied and the link has been visited
* @prop --ion-router-link-hue-bold-semantic-activated-color: Text color of the `bold` hue when a semantic color is applied and the link is pressed
* @prop --ion-router-link-hue-bold-semantic-focus-background: Background of the `bold` hue when a semantic color is applied and the link is focused
* @prop --ion-router-link-hue-subtle-default-color: Text color of the `subtle` hue when no semantic color is applied
* @prop --ion-router-link-hue-subtle-visited-color: Text color of the `subtle` hue when no semantic color is applied and the link has been visited
* @prop --ion-router-link-hue-subtle-activated-color: Text color of the `subtle` hue when no semantic color is applied and the link is pressed
* @prop --ion-router-link-hue-subtle-focus-background: Background of the `subtle` hue when no semantic color is applied and the link is focused
* @prop --ion-router-link-hue-subtle-semantic-default-color: Text color of the `subtle` hue when a semantic color is applied
* @prop --ion-router-link-hue-subtle-semantic-visited-color: Text color of the `subtle` hue when a semantic color is applied and the link has been visited
* @prop --ion-router-link-hue-subtle-semantic-activated-color: Text color of the `subtle` hue when a semantic color is applied and the link is pressed
* @prop --ion-router-link-hue-subtle-semantic-focus-background: Background of the `subtle` hue when a semantic color is applied and the link is focused
* @prop --ion-router-link-state-activated-text-decoration: Text decoration of the router link when it is pressed
* @prop --ion-router-link-state-focus-border-radius: Corner radius of the focus ring and background
* @prop --ion-router-link-state-focus-padding-bottom: Bottom padding of the focus ring and background
* @prop --ion-router-link-state-focus-padding-end: End padding of the focus ring and background
* @prop --ion-router-link-state-focus-padding-start: Start padding of the focus ring and background
* @prop --ion-router-link-state-focus-padding-top: Top padding of the focus ring and background
* @prop --ion-router-link-state-focus-ring-color: Color of the focus ring
* @prop --ion-router-link-state-focus-ring-offset: Space between the focus ring and the router link
* @prop --ion-router-link-state-focus-ring-style: Style of the focus ring
* @prop --ion-router-link-state-focus-ring-width: Width of the focus ring
* @prop --ion-router-link-state-focus-text-decoration: Text decoration of the router link when it is focused
* @prop --ion-router-link-text-decoration: Text decoration of the router link, such as an underline
*/
52 changes: 45 additions & 7 deletions core/src/components/router-link/router-link.interface.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,57 @@
import type { Hue } from '../../themes/themes.interfaces';
import type { Hue, IonPadding } from '../../themes/themes.interfaces';

type IonRouterLinkStateDefinition = {
background?: string;
color?: string;
};

export type IonRouterLinkRecipe = {
hue?: {
[K in Hue]?: {
default?: {
color?: string;
};
activated?: IonRouterLinkStateDefinition;
default?: IonRouterLinkStateDefinition;
focus?: IonRouterLinkStateDefinition;
visited?: IonRouterLinkStateDefinition;

/** Any of the semantic colors like primary, secondary, etc. */
semantic?: {
default?: {
color?: string;
};
activated?: IonRouterLinkStateDefinition;
default?: IonRouterLinkStateDefinition;
focus?: IonRouterLinkStateDefinition;
visited?: IonRouterLinkStateDefinition;
};
};
};

state?: {
activated?: {
text?: {
decoration?: string;
};
};

focus?: {
border?: {
radius?: string;
};

padding?: IonPadding;

ring?: {
color?: string;
offset?: string;
style?: string;
width?: string;
};

text?: {
decoration?: string;
};
};
};

text?: {
decoration?: string;
};
};

Expand Down
97 changes: 89 additions & 8 deletions core/src/components/router-link/router-link.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,106 @@
// Router Link
// --------------------------------------------------

:host {
text-decoration: var(--ion-router-link-text-decoration);
}

a {
@include mixins.text-inherit();

// The theme's focus ring or background below replaces the browser's own ring
outline: none;
}

// Router Link Hues
// --------------------------------------------------

:host(.router-link-hue-bold) {
color: var(--ion-router-link-hue-bold-default-color);
/**
* States are written in this order: default -> visited -> activated.
* The rules share a specificity, so a later state overrides an earlier
* one: activated overrides visited. A state a theme doesn't set falls
* back to the default color.
*/
@each $hue in (bold, subtle) {
:host(.router-link-hue-#{$hue}) {
color: var(--ion-router-link-hue-#{$hue}-default-color);
}

:host(.router-link-hue-#{$hue}) a:visited {
color: var(--ion-router-link-hue-#{$hue}-visited-color, var(--ion-router-link-hue-#{$hue}-default-color));
}

:host(.router-link-hue-#{$hue}.ion-activated) a {
color: var(--ion-router-link-hue-#{$hue}-activated-color, var(--ion-router-link-hue-#{$hue}-default-color));
}

:host(.router-link-hue-#{$hue}.ion-color) {
color: var(--ion-router-link-hue-#{$hue}-semantic-default-color);
}

:host(.router-link-hue-#{$hue}.ion-color) a:visited {
color: var(
--ion-router-link-hue-#{$hue}-semantic-visited-color,
var(--ion-router-link-hue-#{$hue}-semantic-default-color)
);
}

:host(.router-link-hue-#{$hue}.ion-color.ion-activated) a {
color: var(
--ion-router-link-hue-#{$hue}-semantic-activated-color,
var(--ion-router-link-hue-#{$hue}-semantic-default-color)
);
}
}

// Router Link States
// --------------------------------------------------

/**
* States are written in this order: focus -> activated.
* The rules share a specificity, so activated overrides focus.
* A state a theme doesn't set falls back to the default decoration.
*/
:host(.ion-focused) {
text-decoration: var(--ion-router-link-state-focus-text-decoration, var(--ion-router-link-text-decoration));
}

:host(.router-link-hue-subtle) {
color: var(--ion-router-link-hue-subtle-default-color);
:host(.ion-activated) {
text-decoration: var(--ion-router-link-state-activated-text-decoration, var(--ion-router-link-text-decoration));
}

:host(.router-link-hue-bold.ion-color) {
color: var(--ion-router-link-hue-bold-semantic-default-color);
/**
* A theme shows focus with a ring, a background or both. A part
* the theme doesn't set is left out.
*/
:host(.ion-focused) a {
@include mixins.border-radius(var(--ion-router-link-state-focus-border-radius));
@include mixins.padding(
var(--ion-router-link-state-focus-padding-top),
var(--ion-router-link-state-focus-padding-end),
var(--ion-router-link-state-focus-padding-bottom),
var(--ion-router-link-state-focus-padding-start)
);

// Cancels the side padding, so the text doesn't move on focus
@include mixins.margin-horizontal(
calc(-1 * var(--ion-router-link-state-focus-padding-start)),
calc(-1 * var(--ion-router-link-state-focus-padding-end))
);
@include mixins.focused-state(
var(--ion-router-link-state-focus-ring-width),
var(--ion-router-link-state-focus-ring-style),
var(--ion-router-link-state-focus-ring-color),
$offset: var(--ion-router-link-state-focus-ring-offset)
);
}

:host(.router-link-hue-subtle.ion-color) {
color: var(--ion-router-link-hue-subtle-semantic-default-color);
@each $hue in (bold, subtle) {
:host(.router-link-hue-#{$hue}.ion-focused) a {
background: var(--ion-router-link-hue-#{$hue}-focus-background);
}

:host(.router-link-hue-#{$hue}.ion-color.ion-focused) a {
background: var(--ion-router-link-hue-#{$hue}-semantic-focus-background);
}
}
1 change: 1 addition & 0 deletions core/src/components/router-link/router-link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export class RouterLink implements ComponentInterface {
class={createColorClasses(color, {
[`router-link-hue-${hueValue}`]: true,
'ion-activatable': true,
'ion-focusable': true,
})}
>
<a {...attrs} {...tabindexAttributes}>
Expand Down
9 changes: 9 additions & 0 deletions core/src/components/router-link/test/router-link.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@ describe('router-link', () => {
expect(page.root).toHaveClass('ion-activatable');
});

it('should add the ion-focusable class', async () => {
const page = await newSpecPage({
components: [RouterLink],
html: `<ion-router-link href="/page">Link</ion-router-link>`,
});

expect(page.root).toHaveClass('ion-focusable');
});

describe('click', () => {
const setup = async (href: string) => {
const page = await newSpecPage({
Expand Down
62 changes: 62 additions & 0 deletions core/src/components/router-link/test/states/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Router Link - States</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="../../../../../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>
ion-router-link {
margin-inline-end: 16px;
}
</style>
</head>

<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Router Link - States</ion-title>
</ion-toolbar>
</ion-header>

<ion-content class="ion-padding">
<h2>Focused</h2>

<p>
<ion-router-link href="#" class="ion-focused">Default</ion-router-link>
<ion-router-link href="#" class="ion-focused" color="danger">Danger</ion-router-link>
<ion-router-link href="#" class="ion-focused" hue="subtle">Subtle</ion-router-link>
<ion-router-link href="#" class="ion-focused" hue="subtle" color="danger">Subtle danger</ion-router-link>
</p>

<h2>Activated</h2>

<p>
<ion-router-link href="#" class="ion-activated">Default</ion-router-link>
<ion-router-link href="#" class="ion-activated" color="danger">Danger</ion-router-link>
<ion-router-link href="#" class="ion-activated" hue="subtle">Subtle</ion-router-link>
<ion-router-link href="#" class="ion-activated" hue="subtle" color="danger">Subtle danger</ion-router-link>
</p>

<h2>Visited</h2>

<!-- An empty href points at this page, which the browser counts as visited -->
<p>
<ion-router-link href="">Default</ion-router-link>
<ion-router-link href="" color="danger">Danger</ion-router-link>
<ion-router-link href="" hue="subtle">Subtle</ion-router-link>
<ion-router-link href="" hue="subtle" color="danger">Subtle danger</ion-router-link>
</p>
</ion-content>
</ion-app>
</body>
</html>
Loading
Loading