Skip to content
Draft
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
20 changes: 20 additions & 0 deletions BREAKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ This is a comprehensive list of the breaking changes introduced in the major ver
- [Item Divider](#version-9x-item-divider)
- [Menu Toggle](#version-9x-menu-toggle)
- [Radio Group](#version-9x-radio-group)
- [Ripple Effect](#version-9x-ripple-effect)
- [Row](#version-9x-row)
- [Spinner](#version-9x-spinner)
- [Text](#version-9x-text)
Expand Down Expand Up @@ -352,6 +353,25 @@ If you were targeting the internals of `ion-radio-group` in your CSS, you will n

Additionally, the `radio-group-wrapper` div element has been removed, causing slotted elements to be direct children of the `ion-radio-group`.

<h4 id="version-9x-ripple-effect">Ripple Effect</h4>

The following breaking changes apply to `ion-ripple-effect`:

1. The previously undocumented `--ripple-opacity` CSS variable has been renamed to `--ion-ripple-effect-opacity`. <sup>[1](#version-9x-ripple-effect-opacity-variable)</sup>
2. Theme classes (`ion-ripple-effect.md`, `ion-ripple-effect.ios`) are no longer supported. <sup>[2](#version-9x-ripple-effect-theme-classes)</sup>

<h5 id="version-9x-ripple-effect-opacity-variable">Opacity variable</h5>

The ripple fade opacity is now part of the centralized Ionic Theming system. Use the new token structure for global styles, or the corresponding CSS variable for component-specific overrides:

| Old (8.x) | New token (global) | New CSS variable (component-specific) |
|---|---|---|
| `--ripple-opacity` | `IonRippleEffect.opacity` | `--ion-ripple-effect-opacity` |

<h5 id="version-9x-ripple-effect-theme-classes">Theme classes</h5>

Remove any instances that target the theme classes: `ion-ripple-effect.md`, `ion-ripple-effect.ios`.

<h4 id="version-9x-row">Row</h4>

The following breaking changes apply to `ion-row`:
Expand Down
2 changes: 1 addition & 1 deletion core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2230,9 +2230,9 @@ ion-reorder-group,event,ionReorderStart,void,true

ion-ripple-effect,shadow
ion-ripple-effect,prop,mode,"ios" | "md",undefined,false,false
ion-ripple-effect,prop,theme,"ios" | "md" | "ionic",undefined,false,false
ion-ripple-effect,prop,type,"bounded" | "unbounded",'bounded',false,false
ion-ripple-effect,method,addRipple,addRipple(x: number, y: number) => Promise<() => void>
ion-ripple-effect,css-prop,--ion-ripple-effect-opacity

ion-route,none
ion-route,prop,beforeEnter,(() => NavigationHookResult | Promise<NavigationHookResult>) | undefined,undefined,false,false
Expand Down
16 changes: 5 additions & 11 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { RadioGroupChangeEventDetail, RadioGroupCompareFn } from "./components/r
import { PinFormatter, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue } from "./components/range/range-interface";
import { RefresherEventDetail, RefresherPullEndEventDetail } from "./components/refresher/refresher-interface";
import { ItemReorderEventDetail, ReorderEndEventDetail, ReorderMoveEventDetail } from "./components/reorder-group/reorder-group-interface";
import { IonRippleEffectType } from "./components/ripple-effect/ripple-effect.interface";
import { NavigationHookCallback } from "./components/route/route-interface";
import { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface";
import { SegmentChangeEventDetail, SegmentValue } from "./components/segment/segment-interface";
Expand Down Expand Up @@ -80,6 +81,7 @@ export { RadioGroupChangeEventDetail, RadioGroupCompareFn } from "./components/r
export { PinFormatter, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue } from "./components/range/range-interface";
export { RefresherEventDetail, RefresherPullEndEventDetail } from "./components/refresher/refresher-interface";
export { ItemReorderEventDetail, ReorderEndEventDetail, ReorderMoveEventDetail } from "./components/reorder-group/reorder-group-interface";
export { IonRippleEffectType } from "./components/ripple-effect/ripple-effect.interface";
export { NavigationHookCallback } from "./components/route/route-interface";
export { SearchbarChangeEventDetail, SearchbarInputEventDetail } from "./components/searchbar/searchbar-interface";
export { SegmentChangeEventDetail, SegmentValue } from "./components/segment/segment-interface";
Expand Down Expand Up @@ -3318,15 +3320,11 @@ export namespace Components {
* The mode determines the platform behaviors of the component.
*/
"mode"?: "ios" | "md";
/**
* The theme determines the visual appearance of the component.
*/
"theme"?: "ios" | "md" | "ionic";
/**
* Sets the type of ripple-effect: - `bounded`: the ripple effect expands from the user's click position - `unbounded`: the ripple effect expands from the center of the button and overflows the container. NOTE: Surfaces for bounded ripples should have the overflow property set to hidden, while surfaces for unbounded ripples should have it set to visible.
* @default 'bounded'
*/
"type": 'bounded' | 'unbounded';
"type": IonRippleEffectType;
}
interface IonRoute {
/**
Expand Down Expand Up @@ -9354,15 +9352,11 @@ declare namespace LocalJSX {
* The mode determines the platform behaviors of the component.
*/
"mode"?: "ios" | "md";
/**
* The theme determines the visual appearance of the component.
*/
"theme"?: "ios" | "md" | "ionic";
/**
* Sets the type of ripple-effect: - `bounded`: the ripple effect expands from the user's click position - `unbounded`: the ripple effect expands from the center of the button and overflows the container. NOTE: Surfaces for bounded ripples should have the overflow property set to hidden, while surfaces for unbounded ripples should have it set to visible.
* @default 'bounded'
*/
"type"?: 'bounded' | 'unbounded';
"type"?: IonRippleEffectType;
}
interface IonRoute {
/**
Expand Down Expand Up @@ -11183,7 +11177,7 @@ declare namespace LocalJSX {
"disabled": boolean;
}
interface IonRippleEffectAttributes {
"type": 'bounded' | 'unbounded';
"type": IonRippleEffectType;
}
interface IonRouteAttributes {
"url": string;
Expand Down
11 changes: 8 additions & 3 deletions core/src/components/button/button.ionic.scss
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
--background-hover-opacity: 1;
--background: #{globals.ion-color(primary, base)};
--color: #{globals.ion-color(primary, contrast)};
--ripple-opacity: var(--background-activated-opacity, 1);
--ripple-color: var(--background-activated);
}

Expand All @@ -65,7 +64,6 @@
--background-hover-opacity: 1;
--border-color: #{globals.ion-color(primary, base)};
--color: #{globals.ion-color(primary, base)};
--ripple-opacity: var(--background-activated-opacity, 1);
--ripple-color: var(--background-activated);
}

Expand All @@ -85,13 +83,20 @@
--background-hover: #{globals.ion-color(primary, shade, $subtle: true)};
--background-hover-opacity: 1;
--color: #{globals.ion-color(primary, foreground)};
--ripple-opacity: var(--background-activated-opacity, 1);
--ripple-color: var(--background-activated);
}

// Ripple Effect
// -------------------------------------------------------------------------------

// Set via the custom property (not `opacity` directly) because the ripple's
// opacity is animated in @keyframes, which would override a direct value.
:host(.button-solid) ion-ripple-effect,
:host(.button-outline) ion-ripple-effect,
:host(.button-clear) ion-ripple-effect {
--ion-ripple-effect-opacity: var(--background-activated-opacity, 1);
}

:host(.button-solid.ion-color) ion-ripple-effect {
color: globals.current-color(shade);
}
Expand Down
79 changes: 0 additions & 79 deletions core/src/components/ripple-effect/ripple-effect.common.scss

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type IonRippleEffectRecipe = {
opacity?: string;
};

export const ION_RIPPLE_EFFECT_TYPES = ['bounded', 'unbounded'] as const;
export type IonRippleEffectType = (typeof ION_RIPPLE_EFFECT_TYPES)[number];
49 changes: 0 additions & 49 deletions core/src/components/ripple-effect/ripple-effect.ionic.scss

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While consolidating the ripple-effect theme files, the only difference in the rippleAnimation keyframe between ionic and ios/md was the easing curve:

  • ios/md used cubic-bezier(0.4, 0, 0.2, 1) directly
  • ionic used $ion-transition-curve-expressive

That ionic variable is defined as var(--token-transition-curve-expressive, cubic-bezier(0.4, 0, 0.2, 1)), so it resolves to the exact same curve by default. All three themes animate identically.

I chose to hardcode cubic-bezier(0.4, 0, 0.2, 1) in the consolidated file rather than introduce a --ion-ripple-effect-* token for it, because:

  1. No visual difference to express. A token's purpose is to let a value vary per theme or be customized. Here the value is identical across all three themes, so a token would add public API surface and a recipe/token entry in every theme file for zero behavioral gain.
  2. Consistency cost. If we tokenized this easing, we'd owe matching tokens for the other keyframe easings (fadeInAnimation/fadeOutAnimation both use linear) to avoid an arbitrary split — more surface area, still no visual difference.

If we ever need the ripple easing to be theme-customizable, then we can create the token at that time.

This file was deleted.

94 changes: 94 additions & 0 deletions core/src/components/ripple-effect/ripple-effect.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
@use "../../themes/mixins" as mixins;

// Ripple Effect: Common Styles
// --------------------------------------------------

/*
* Duration of the ripple scale animation in milliseconds. This MUST stay in
* sync with SCALE_DURATION in ripple-effect.tsx: the ripple cleanup is
* scheduled off this value, so changing one without the other desyncs the
* animation from the removal timing.
*/
$scale-duration: 225ms;
$fade-in-duration: 75ms;
$fade-out-duration: 150ms;

:host {
/**
* @prop --ion-ripple-effect-opacity: Peak opacity the ripple fades in to.
*/

@include mixins.position(0, 0, 0, 0);

position: absolute;

contain: strict;
pointer-events: none;
}

:host(.unbounded) {
contain: layout size style;
}

.ripple-effect {
@include mixins.border-radius(50%);

position: absolute;

/*
* Keep `background-color` and `color` as static keywords (never `var()`)
* for performance issues. A new .ripple-effect div is created and animated
* on every tap, so a custom property lookup for every ripple would be
* costly. The color is inherited from its host.
*/
background-color: currentColor;
color: inherit;

contain: strict;
opacity: 0;
animation: $scale-duration rippleAnimation forwards, $fade-in-duration fadeInAnimation forwards;

will-change: transform, opacity;
pointer-events: none;
}

.fade-out {
transform: translate(var(--internal-translate-end)) scale(var(--internal-final-scale, 1));
animation: $fade-out-duration fadeOutAnimation forwards;
}

@keyframes rippleAnimation {
from {
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);

transform: scale(1);
}

to {
transform: translate(var(--internal-translate-end)) scale(var(--internal-final-scale, 1));
}
}

@keyframes fadeInAnimation {
from {
animation-timing-function: linear;

opacity: 0;
}

to {
opacity: var(--ion-ripple-effect-opacity, 0.16);
}
}

@keyframes fadeOutAnimation {
from {
animation-timing-function: linear;

opacity: var(--ion-ripple-effect-opacity, 0.16);
}

to {
opacity: 0;
}
}
Loading
Loading