feat(badge): add support for hint feature (#30213)

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 new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Added new position prop.
- Based on the prop, a new badge-position css class is added on the
host.
- Removed css rule that prevented the badge to be rendered when empty.
- Added common and themes styles to support the position prop and new
scale size (on ios/md defaults to min-width variable, as size is not yet
supported on native themes).
- Added new tests specific for this new feature and added new
screenshots.
- Support to properly work inside Avatar, Button and TabButton will be
added on different tasks (in the meantime the snapshots will appear
wrong for these use-cases)

## 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.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/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. -->

- [Ionic Theme
Sample](https://ionic-framework-6n0cn175k-ionic1.vercel.app/src/components/badge/test/hint?ionic:theme=ionic)
- [MD Theme
Sample](https://ionic-framework-6n0cn175k-ionic1.vercel.app/src/components/badge/test/hint?ionic:theme=md)
- [iOS Theme
Sample](https://ionic-framework-6n0cn175k-ionic1.vercel.app/src/components/badge/test/hint?ionic:theme=ios)

---------

Co-authored-by: ionitron <hi@ionicframework.com>
Co-authored-by: Marta Carlos <101343976+OS-martacarlos@users.noreply.github.com>
Co-authored-by: Brandy Smith <brandyscarney@users.noreply.github.com>
This commit is contained in:
Bernardo Cardoso
2025-03-03 18:15:27 +00:00
committed by GitHub
parent 5eab76c0f3
commit 517d5b99b2
53 changed files with 238 additions and 7 deletions

View File

@@ -317,6 +317,7 @@ ion-badge,prop,mode,"ios" | "md",undefined,false,false
ion-badge,prop,shape,"round | rectangular" | "soft" | undefined,undefined,false,false
ion-badge,prop,size,"large" | "medium" | "small" | "xlarge" | "xsmall" | "xxsmall" | undefined,undefined,false,false
ion-badge,prop,theme,"ios" | "md" | "ionic",undefined,false,false
ion-badge,prop,vertical,"bottom" | "top" | undefined,undefined,false,false
ion-badge,css-prop,--background,ionic
ion-badge,css-prop,--background,ios
ion-badge,css-prop,--background,md

View File

@@ -441,6 +441,10 @@ export namespace Components {
* The theme determines the visual appearance of the component.
*/
"theme"?: "ios" | "md" | "ionic";
/**
* Set to `"top"` to position the badge on top right absolute position of the parent element. Set to `"bottom"` to position the badge on bottom right absolute position of the parent element.
*/
"vertical"?: 'top' | 'bottom';
}
interface IonBreadcrumb {
/**
@@ -5805,6 +5809,10 @@ declare namespace LocalJSX {
* The theme determines the visual appearance of the component.
*/
"theme"?: "ios" | "md" | "ionic";
/**
* Set to `"top"` to position the badge on top right absolute position of the parent element. Set to `"bottom"` to position the badge on bottom right absolute position of the parent element.
*/
"vertical"?: 'top' | 'bottom';
}
interface IonBreadcrumb {
/**

View File

@@ -10,6 +10,8 @@
@include border-radius(var(--border-radius));
display: block;
position: relative;
}
::slotted(ion-img),

View File

@@ -152,3 +152,22 @@
width: globals.$ion-scale-1000;
height: globals.$ion-scale-1000;
}
// Avatar Empty Badge (hint)
// --------------------------------------------------
:host(.avatar-xxsmall) ::slotted(ion-badge.badge-vertical-top:empty) {
transform: translate(globals.$ion-scale-100, calc(globals.$ion-scale-100 * -1));
}
:host(:not(.avatar-xxsmall)) ::slotted(ion-badge.badge-vertical-top:empty) {
transform: translate(globals.$ion-scale-050, calc(globals.$ion-scale-050 * -1));
}
:host(.avatar-xxsmall) ::slotted(ion-badge.badge-vertical-bottom:empty) {
transform: translate(globals.$ion-scale-100, calc(globals.$ion-scale-100));
}
:host(:not(.avatar-xxsmall)) ::slotted(ion-badge.badge-vertical-bottom:empty) {
transform: translate(0, globals.$ion-scale-100);
}

View File

@@ -10,3 +10,14 @@
width: $avatar-md-width;
height: $avatar-md-height;
}
// Avatar Empty Badge (hint)
// --------------------------------------------------
::slotted(ion-badge.badge-vertical-top:empty) {
transform: translate(-50%, 50%);
}
::slotted(ion-badge.badge-vertical-bottom:empty) {
transform: translateX(-100%);
}

View File

@@ -48,6 +48,18 @@
color: #{color.current-color(contrast)};
}
:host(:empty) {
display: none;
// Badge Empty (hint)
// --------------------------------------------------
:host([vertical]) {
@include position(null, 0, null, null);
position: absolute;
}
:host(:empty.badge-vertical-top) {
top: 0;
}
:host(:empty.badge-vertical-bottom) {
bottom: 0;
}

View File

@@ -145,3 +145,32 @@
width: globals.$ion-scale-1000;
height: globals.$ion-scale-1000;
}
// Badge Empty
// --------------------------------------------------
:host(:empty) {
--padding-start: 0;
--padding-end: 0;
}
// Badge Sizes Empty
// --------------------------------------------------
/* sm */
:host(.badge-small:empty) {
min-width: globals.$ion-scale-200;
height: globals.$ion-scale-200;
}
/* md */
:host(.badge-medium:empty) {
min-width: globals.$ion-scale-300;
height: globals.$ion-scale-300;
}
/* lg */
:host(.badge-large:empty) {
min-width: globals.$ion-scale-400;
height: globals.$ion-scale-400;
}

View File

@@ -11,3 +11,16 @@
font-family: $font-family-base;
}
// TODO(ROU-10747): Review size styles when sizes are defined for native themes.
:host(:empty) {
--padding-start: 0;
--padding-end: 0;
--padding-bottom: 0;
--padding-top: 0;
@include border-radius(999px);
width: $badge-min-width;
height: $badge-min-width;
}

View File

@@ -47,6 +47,12 @@ export class Badge implements ComponentInterface {
*/
@Prop() size?: 'xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge';
/**
* Set to `"top"` to position the badge on top right absolute position of the parent element.
* Set to `"bottom"` to position the badge on bottom right absolute position of the parent element.
*/
@Prop() vertical?: 'top' | 'bottom';
private getShape(): string | undefined {
const theme = getIonTheme(this);
const { shape } = this;
@@ -89,6 +95,7 @@ export class Badge implements ComponentInterface {
[theme]: true,
[`badge-${shape}`]: shape !== undefined,
[`badge-${size}`]: size !== undefined,
[`badge-vertical-${this.vertical}`]: this.vertical !== undefined,
})}
>
<slot></slot>

View File

@@ -0,0 +1,24 @@
import { expect } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
configs({ directions: ['ltr'], modes: ['md', 'ios', 'ionic-md'] }).forEach(({ config, screenshot, title }) => {
test.describe(title('badge: hint empty'), () => {
test('should not have visual regressions', async ({ page }) => {
await page.goto('/src/components/badge/test/hint', config);
const container = page.locator('#empty');
await expect(container).toHaveScreenshot(screenshot(`badge-hint-empty`));
});
});
test.describe(title('badge: hint inside avatar'), () => {
test('should not have visual regressions', async ({ page }) => {
await page.goto('/src/components/badge/test/hint', config);
const container = page.locator('#avatar');
await expect(container).toHaveScreenshot(screenshot(`badge-hint-avatar`));
});
});
});

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -0,0 +1,104 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Badge - Hint</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>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Badge - Hint</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list id="empty">
<ion-list-header>
<ion-label> Badge Empty </ion-label>
</ion-list-header>
<ion-item>
<ion-label>Badge small</ion-label>
<ion-badge color="primary" size="small"></ion-badge>
</ion-item>
<ion-item>
<ion-label>Badge Medium</ion-label>
<ion-badge color="danger" size="medium"></ion-badge>
</ion-item>
<ion-item>
<ion-label>Badge Large</ion-label>
<ion-badge color="warning" size="large"></ion-badge>
</ion-item>
</ion-list>
<ion-list id="avatar">
<ion-list-header>
<ion-label> Inside Avatar </ion-label>
</ion-list-header>
<div class="ion-display-flex ion-align-items-center ion-justify-content-around ion-margin">
<ion-avatar size="xxsmall">
<img src="/src/components/avatar/test/avatar.svg" />
<ion-badge color="danger" size="small" vertical="top"></ion-badge>
</ion-avatar>
<ion-avatar size="xsmall">
<img src="/src/components/avatar/test/avatar.svg" />
<ion-badge color="danger" size="small" vertical="top"></ion-badge>
</ion-avatar>
<ion-avatar size="small">
<img src="/src/components/avatar/test/avatar.svg" />
<ion-badge color="danger" size="medium" vertical="top"></ion-badge>
</ion-avatar>
<ion-avatar size="medium">
<img src="/src/components/avatar/test/avatar.svg" />
<ion-badge color="danger" size="medium" vertical="top"></ion-badge>
</ion-avatar>
<ion-avatar size="large">
<img src="/src/components/avatar/test/avatar.svg" />
<ion-badge color="danger" size="medium" vertical="top"></ion-badge>
</ion-avatar>
<ion-avatar size="xlarge">
<img src="/src/components/avatar/test/avatar.svg" />
<ion-badge color="danger" size="large" vertical="top"></ion-badge>
</ion-avatar>
</div>
<div class="ion-display-flex ion-align-items-center ion-justify-content-around ion-margin">
<ion-avatar size="xxsmall">
<img src="/src/components/avatar/test/avatar.svg" />
<ion-badge color="danger" size="small" vertical="bottom"></ion-badge>
</ion-avatar>
<ion-avatar size="xsmall">
<img src="/src/components/avatar/test/avatar.svg" />
<ion-badge color="danger" size="small" vertical="bottom"></ion-badge>
</ion-avatar>
<ion-avatar size="small">
<img src="/src/components/avatar/test/avatar.svg" />
<ion-badge color="danger" size="medium" vertical="bottom"></ion-badge>
</ion-avatar>
<ion-avatar size="medium">
<img src="/src/components/avatar/test/avatar.svg" />
<ion-badge color="danger" size="medium" vertical="bottom"></ion-badge>
</ion-avatar>
<ion-avatar size="large">
<img src="/src/components/avatar/test/avatar.svg" />
<ion-badge color="danger" size="medium" vertical="bottom"></ion-badge>
</ion-avatar>
<ion-avatar size="xlarge">
<img src="/src/components/avatar/test/avatar.svg" />
<ion-badge color="danger" size="large" vertical="bottom"></ion-badge>
</ion-avatar>
</div>
</ion-list>
</ion-content>
</ion-app>
</body>
</html>

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -261,14 +261,14 @@ export declare interface IonBackdrop extends Components.IonBackdrop {
@ProxyCmp({
inputs: ['color', 'mode', 'shape', 'size', 'theme']
inputs: ['color', 'mode', 'shape', 'size', 'theme', 'vertical']
})
@Component({
selector: 'ion-badge',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['color', 'mode', 'shape', 'size', 'theme'],
inputs: ['color', 'mode', 'shape', 'size', 'theme', 'vertical'],
})
export class IonBadge {
protected el: HTMLElement;

View File

@@ -349,14 +349,14 @@ export declare interface IonBackdrop extends Components.IonBackdrop {
@ProxyCmp({
defineCustomElementFn: defineIonBadge,
inputs: ['color', 'mode', 'shape', 'size', 'theme']
inputs: ['color', 'mode', 'shape', 'size', 'theme', 'vertical']
})
@Component({
selector: 'ion-badge',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['color', 'mode', 'shape', 'size', 'theme'],
inputs: ['color', 'mode', 'shape', 'size', 'theme', 'vertical'],
standalone: true
})
export class IonBadge {

View File

@@ -124,7 +124,8 @@ export const IonBackdrop = /*@__PURE__*/ defineContainer<JSX.IonBackdrop>('ion-b
export const IonBadge = /*@__PURE__*/ defineContainer<JSX.IonBadge>('ion-badge', defineIonBadge, [
'color',
'shape',
'size'
'size',
'vertical'
]);