feat(tab-button): support new badge hint features (#30246)

Issue number: internal

## What is the current behavior?
- tab-button with badge only supports status badges (small circle with
specific color), missing cases with text or icon.

## What is the new behavior?
- Added css on tab-button to support new badge hint functionalities such
text and icon
- Added css on badge to support the existence of a icon inside it
- Added support on ionic theme for different position (top and button)
- Added badge test case for this use case
- There's no figma for md/iOS implementation, so just added for now a
best effort on how it looks.

**Theme md:**
<img width="418" alt="md"
src="https://github.com/user-attachments/assets/0281b616-12aa-4107-bf4c-e8fdbe72d7d3"
/>


**Theme iOS:**
<img width="417" alt="ios"
src="https://github.com/user-attachments/assets/6102d874-b608-4527-91fb-4ed8b7e536a0"
/>


**Theme ionic:**
<img width="418" alt="ionic"
src="https://github.com/user-attachments/assets/10f59d57-0c30-4f04-bbe8-de625b93eb46"
/>


### Notes:
- All the styles added are based on the [vertical] attribute in order to
prevent breaking changes and only affect the badge hint with the new
features implemented.
- Also top and bottom position under the context of md/iOS themes are
not exactly the same as ionic theme due the usage of an
`overflow:hidden;` attribute at the `.button-native` context. This
`overflow:hidden;` attribute has been overwritten to `overflow:visible;`
for ionic theme.

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

## Other information


[Sample](https://ionic-framework-git-rou-11666-ionic1.vercel.app/src/components/badge/test/hint)
This commit is contained in:
José Rio
2025-03-21 17:44:29 +00:00
committed by GitHub
parent a7233c296a
commit c5fb051be3
45 changed files with 403 additions and 9 deletions

View File

@@ -51,15 +51,15 @@
// Badge Empty (hint)
// --------------------------------------------------
:host([vertical]) {
:host([vertical]:not(.in-tab-button)) {
@include position(null, 0, null, null);
position: absolute;
}
:host(:empty.badge-vertical-top) {
:host(:not(.in-tab-button):empty.badge-vertical-top) {
top: 0;
}
:host(:empty.badge-vertical-bottom) {
:host(:not(.in-tab-button):empty.badge-vertical-bottom) {
bottom: 0;
}

View File

@@ -174,3 +174,19 @@
min-width: globals.$ion-scale-400;
height: globals.$ion-scale-400;
}
// Badge Inside Tab Button
// --------------------------------------------------
:host([vertical].in-tab-button) {
position: relative;
}
// Badge Icon when Inside Tab Button
:host([vertical].in-tab-button) ::slotted(ion-icon) {
@include globals.position(50%, null, null, 50%);
position: absolute;
transform: translate(-50%, -50%);
}

View File

@@ -16,3 +16,23 @@
*/
font-size: dynamic-font-min(1, $badge-baseline-font-size);
}
// Badge Inside Tab Button
// --------------------------------------------------
:host([vertical].in-tab-button) {
position: relative;
min-width: $badge-ios-in-tab-button-min-width;
}
:host([vertical].in-tab-button) ::slotted(ion-icon) {
@include position(50%, null, null, 50%);
position: absolute;
width: $badge-ios-in-tab-button-icon-size;
height: $badge-ios-in-tab-button-icon-size;
transform: translate(-50%, -50%);
}

View File

@@ -5,3 +5,15 @@
/// @prop - Border radius of the badge
$badge-ios-border-radius: 10px;
// Badge inside a Tab Button
// --------------------------------------------------
/// @prop - Minimum width of the badge inside a Tab Button
$badge-ios-in-tab-button-min-width: 8px;
/// @prop - Height of the badge inside a Tab Button
$badge-ios-in-tab-button-height: 8px;
/// @prop - Badge icon size inside a Tab Button
$badge-ios-in-tab-button-icon-size: 12px;

View File

@@ -1,6 +1,6 @@
import type { ComponentInterface } from '@stencil/core';
import { Component, Host, Prop, h } from '@stencil/core';
import { createColorClasses } from '@utils/theme';
import { Component, Element, Host, Prop, h } from '@stencil/core';
import { createColorClasses, hostContext } from '@utils/theme';
import { getIonTheme } from '../../global/ionic-global';
import type { Color } from '../../interface';
@@ -19,6 +19,8 @@ import type { Color } from '../../interface';
shadow: true,
})
export class Badge implements ComponentInterface {
@Element() el!: HTMLElement;
/**
* The color to use from your application's color palette.
* Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`.
@@ -89,6 +91,7 @@ export class Badge implements ComponentInterface {
const shape = this.getShape();
const size = this.getSize();
const theme = getIonTheme(this);
return (
<Host
class={createColorClasses(this.color, {
@@ -96,6 +99,7 @@ export class Badge implements ComponentInterface {
[`badge-${shape}`]: shape !== undefined,
[`badge-${size}`]: size !== undefined,
[`badge-vertical-${this.vertical}`]: this.vertical !== undefined,
'in-tab-button': hostContext('ion-tab-button', this.el),
})}
>
<slot></slot>

View File

@@ -21,4 +21,22 @@ configs({ directions: ['ltr'], modes: ['md', 'ios', 'ionic-md'] }).forEach(({ co
await expect(container).toHaveScreenshot(screenshot(`badge-hint-avatar`));
});
});
test.describe(title('badge: hint inside tab button'), () => {
test('should not have visual regressions when icon is on the top', async ({ page }) => {
await page.goto('/src/components/badge/test/hint', config);
const container = page.locator('#tab-button');
await expect(container).toHaveScreenshot(screenshot(`badge-hint-tab-button-icon-top`));
});
test('should not have visual regressions when icon is at the bottom', async ({ page }) => {
await page.goto('/src/components/badge/test/hint', config);
const container = page.locator('#tab-button-icon-bottom');
await expect(container).toHaveScreenshot(screenshot(`badge-hint-tab-button-icon-bottom`));
});
});
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -12,6 +12,12 @@
<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-tab-bar {
width: 100%;
}
</style>
</head>
<body>
@@ -45,6 +51,7 @@
<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" />
@@ -98,6 +105,136 @@
</ion-avatar>
</div>
</ion-list>
<ion-list id="tab-button">
<ion-list-header>
<ion-label> Inside Tab Button - Top Icon </ion-label>
</ion-list-header>
<div class="ion-display-flex ion-align-items-center ion-justify-content-around ion-margin">
<ion-tab-bar>
<ion-tab-button href="" tab="tab-one">
<ion-label>Tab One</ion-label>
<ion-icon name="star"></ion-icon>
<ion-badge size="xxsmall" vertical="top" color="danger">9</ion-badge>
</ion-tab-button>
<ion-tab-button tab="schedule" class="e2eTabTwoButton">
<ion-label>Tab Two</ion-label>
<ion-icon name="globe"></ion-icon>
<ion-badge size="xxsmall" vertical="top" color="primary">
<ion-icon name="star"></ion-icon>
</ion-badge>
</ion-tab-button>
<ion-tab-button tab="tab-three">
<ion-label>Tab Three</ion-label>
<ion-icon name="logo-facebook"></ion-icon>
<ion-badge size="xxsmall" vertical="top" color="warning">999</ion-badge>
</ion-tab-button>
<ion-tab-button tab="tab-four" class="e2eTabFourButton">
<ion-label>Tab Four</ion-label>
<ion-icon name="chatbox"></ion-icon>
<ion-badge vertical="top" color="success" size="small"></ion-badge>
</ion-tab-button>
</ion-tab-bar>
</div>
<div class="ion-display-flex ion-align-items-center ion-justify-content-around ion-margin">
<ion-tab-bar>
<ion-tab-button href="" tab="tab-one">
<ion-label>Tab One</ion-label>
<ion-icon name="star"></ion-icon>
<ion-badge size="xxsmall" vertical="bottom" color="danger">9</ion-badge>
</ion-tab-button>
<ion-tab-button tab="schedule" class="e2eTabTwoButton">
<ion-label>Tab Two</ion-label>
<ion-icon name="globe"></ion-icon>
<ion-badge size="xxsmall" vertical="bottom" color="primary">
<ion-icon name="star"></ion-icon>
</ion-badge>
</ion-tab-button>
<ion-tab-button tab="tab-three">
<ion-label>Tab Three</ion-label>
<ion-icon name="logo-facebook"></ion-icon>
<ion-badge size="xxsmall" vertical="bottom" color="warning">999</ion-badge>
</ion-tab-button>
<ion-tab-button tab="tab-four" class="e2eTabFourButton">
<ion-label>Tab Four</ion-label>
<ion-icon name="chatbox"></ion-icon>
<ion-badge vertical="bottom" color="success" size="small"></ion-badge>
</ion-tab-button>
</ion-tab-bar>
</div>
</ion-list>
<ion-list id="tab-button-icon-bottom">
<ion-list-header>
<ion-label> Inside Tab Button - Bottom Icon </ion-label>
</ion-list-header>
<div class="ion-display-flex ion-align-items-center ion-justify-content-around ion-margin">
<ion-tab-bar>
<ion-tab-button href="" tab="tab-one" layout="icon-bottom">
<ion-label>Tab One</ion-label>
<ion-icon name="star"></ion-icon>
<ion-badge size="xxsmall" vertical="top" color="danger">9</ion-badge>
</ion-tab-button>
<ion-tab-button tab="schedule" class="e2eTabTwoButton" layout="icon-bottom">
<ion-label>Tab Two</ion-label>
<ion-icon name="globe"></ion-icon>
<ion-badge size="xxsmall" vertical="top" color="primary">
<ion-icon name="star"></ion-icon>
</ion-badge>
</ion-tab-button>
<ion-tab-button tab="tab-three" layout="icon-bottom">
<ion-label>Tab Three</ion-label>
<ion-icon name="logo-facebook"></ion-icon>
<ion-badge size="xxsmall" vertical="top" color="warning">999</ion-badge>
</ion-tab-button>
<ion-tab-button tab="tab-four" class="e2eTabFourButton" layout="icon-bottom">
<ion-label>Tab Four</ion-label>
<ion-icon name="chatbox"></ion-icon>
<ion-badge vertical="top" color="success" size="small"></ion-badge>
</ion-tab-button>
</ion-tab-bar>
</div>
<div class="ion-display-flex ion-align-items-center ion-justify-content-around ion-margin">
<ion-tab-bar>
<ion-tab-button href="" tab="tab-one" layout="icon-bottom">
<ion-label>Tab One</ion-label>
<ion-icon name="star"></ion-icon>
<ion-badge size="xxsmall" vertical="bottom" color="danger">9</ion-badge>
</ion-tab-button>
<ion-tab-button tab="schedule" class="e2eTabTwoButton" layout="icon-bottom">
<ion-label>Tab Two</ion-label>
<ion-icon name="globe"></ion-icon>
<ion-badge size="xxsmall" vertical="bottom" color="primary">
<ion-icon name="star"></ion-icon>
</ion-badge>
</ion-tab-button>
<ion-tab-button tab="tab-three" layout="icon-bottom">
<ion-label>Tab Three</ion-label>
<ion-icon name="logo-facebook"></ion-icon>
<ion-badge size="xxsmall" vertical="bottom" color="warning">999</ion-badge>
</ion-tab-button>
<ion-tab-button tab="tab-four" class="e2eTabFourButton" layout="icon-bottom">
<ion-label>Tab Four</ion-label>
<ion-icon name="chatbox"></ion-icon>
<ion-badge vertical="bottom" color="success" size="small"></ion-badge>
</ion-tab-button>
</ion-tab-bar>
</div>
</ion-list>
</ion-content>
</ion-app>
</body>

View File

@@ -27,6 +27,8 @@
@include globals.margin(0);
@include globals.padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start));
@include globals.text-inherit();
overflow: visible;
}
.button-native::after {
@@ -86,3 +88,47 @@
:host(.tab-button-shape-rectangular) {
@include globals.border-radius(globals.$ion-border-radius-0);
}
// Tab Button: Badge
// --------------------------------------------------
:host ::slotted(ion-badge) {
@include globals.position(null, null, null, calc(50% + globals.$ion-scale-300));
}
:host ::slotted(ion-badge.badge-vertical-top) {
@include globals.position(-2px);
}
:host ::slotted(ion-badge.badge-vertical-top:empty) {
@include globals.position(calc(globals.$ion-scale-100 * -1));
}
:host ::slotted(ion-badge.badge-vertical-bottom) {
@include globals.position(calc(50% - globals.$ion-scale-200));
}
:host ::slotted(ion-badge.badge-vertical-bottom:empty) {
@include globals.position(calc(50% - globals.$ion-scale-100));
}
:host ::slotted(ion-badge[vertical]:not(:empty)) {
@include globals.padding(globals.$ion-scale-100);
display: flex;
align-items: center;
justify-content: center;
}
:host(.tab-layout-icon-bottom) ::slotted(ion-badge.badge-vertical-top) {
@include globals.position(calc(50% - globals.$ion-scale-100));
}
:host(.tab-layout-icon-bottom) ::slotted(ion-badge.badge-vertical-bottom) {
@include globals.position(calc(50% + globals.$ion-scale-100));
}
:host(.tab-layout-icon-bottom) ::slotted(ion-badge.badge-vertical-bottom:empty) {
@include globals.position(calc(50% + 14px));
}

View File

@@ -15,7 +15,7 @@
// iOS Tab Button: Badge
// --------------------------------------------------
::slotted(ion-badge) {
::slotted(ion-badge:not([vertical])) {
@include padding(1px, 6px);
@include position(4px, null, null, calc(50% + 6px));
@@ -26,6 +26,64 @@
line-height: 16px;
}
:host ::slotted(ion-badge:empty) {
@include padding(0, 0);
width: $tab-ios-button-badge-size-emtpy;
height: $tab-ios-button-badge-size-emtpy;
}
:host ::slotted(ion-badge[vertical]) {
@include position(null, null, null, $tab-ios-button-badge-start-position);
}
:host ::slotted(ion-badge.badge-vertical-top) {
@include position($tab-ios-button-badge-top-position);
}
:host ::slotted(ion-badge.badge-vertical-top:empty) {
@include position($tab-ios-button-badge-top-position-empty);
}
:host ::slotted(ion-badge.badge-vertical-bottom) {
@include position($tab-ios-button-badge-bottom-position);
}
:host ::slotted(ion-badge.badge-vertical-bottom:empty) {
@include position($tab-ios-button-badge-bottom-position-empty);
}
:host ::slotted(ion-badge[vertical]:not(:empty)) {
@include padding(
$tab-ios-button-badge-padding-top,
$tab-ios-button-badge-padding-end,
$tab-ios-button-badge-padding-bottom,
$tab-ios-button-badge-padding-start
);
display: flex;
align-items: center;
justify-content: center;
min-width: $tab-ios-button-badge-size;
height: $tab-ios-button-badge-size;
font-size: $tab-ios-button-badge-font-size;
}
:host(.tab-layout-icon-bottom) ::slotted(ion-badge.badge-vertical-top) {
@include position($tab-ios-button-icon-bottom-badge-top-position);
}
:host(.tab-layout-icon-bottom) ::slotted(ion-badge.badge-vertical-bottom) {
@include position($tab-ios-button-icon-bottom-badge-bottom-position);
}
:host(.tab-layout-icon-bottom) ::slotted(ion-badge.badge-vertical-bottom:empty) {
@include position($tab-ios-button-icon-bottom-badge-bottom-position-empty);
}
// iOS Tab Button: Icon
// --------------------------------------------------
@@ -102,7 +160,7 @@
// Label
:host(.tab-layout-icon-bottom) ::slotted(ion-label) {
@include margin(4px, null, null, null);
@include margin(4px);
}
// iOS Tab Button: Icon Start / End Layout
@@ -128,7 +186,7 @@
// Badge
:host(.tab-layout-label-hide) ::slotted(ion-badge),
:host(.tab-has-icon-only) ::slotted(ion-badge) {
@include position(10px, null, null, null);
@include position(10px);
}
// Icon

View File

@@ -29,3 +29,51 @@ $tab-button-ios-icon-size: 24px;
/// @prop - Size of the tab button icon when there is not a label
$tab-button-ios-icon-only-size: 30px;
// Tab Button: Badge
// --------------------------------------------------
/// @prop - Badge size
$tab-ios-button-badge-size: 16px;
/// @prop - Badge size when empty
$tab-ios-button-badge-size-emtpy: 8px;
/// @prop - Badge top position
$tab-ios-button-badge-top-position: 2px;
/// @prop - Badge top position when empty
$tab-ios-button-badge-top-position-empty: 4px;
/// @prop - Badge bottom position
$tab-ios-button-badge-bottom-position: calc(50% - 8px);
/// @prop - Badge bottom position when empty
$tab-ios-button-badge-bottom-position-empty: calc(50% - 2px);
/// @prop - Badge left position
$tab-ios-button-badge-start-position: calc(50% + 12px);
/// @prop - Badge padding top
$tab-ios-button-badge-padding-top: 4px;
/// @prop - Badge padding end
$tab-ios-button-badge-padding-end: 4px;
/// @prop - Badge padding bottom
$tab-ios-button-badge-padding-bottom: 4px;
/// @prop - Badge padding start
$tab-ios-button-badge-padding-start: 4px;
/// @prop - Badge font size
$tab-ios-button-badge-font-size: 12px;
/// @prop - Badge bottom position when tab bottom has icon at Bottom
$tab-ios-button-icon-bottom-badge-top-position: calc(50% - 4px);
/// @prop - Badge bottom position when tab bottom has icon at bottom
$tab-ios-button-icon-bottom-badge-bottom-position: calc(50% + 4px);
/// @prop - Badge bottom position when empty and when tab bottom has icon at bottom
$tab-ios-button-icon-bottom-badge-bottom-position-empty: calc(50% + 14px);

View File

@@ -75,6 +75,26 @@
height: $tab-button-md-badge-size-empty;
}
:host ::slotted(ion-badge[vertical="bottom"]) {
@include position($tab-button-md-badge-bottom-position);
}
:host ::slotted(ion-badge[vertical="bottom"]:empty) {
@include position($tab-button-md-badge-bottom-position-empty);
}
:host(.tab-layout-icon-bottom) ::slotted(ion-badge[vertical="top"]) {
@include position($tab-md-button-icon-bottom-badge-top-position, null, null, calc(50% + 6px));
}
:host(.tab-layout-icon-bottom) ::slotted(ion-badge[vertical="bottom"]) {
@include position($tab-md-button-icon-bottom-badge-bottom-position, null, null, calc(50% + 6px));
}
:host(.tab-layout-icon-bottom) ::slotted(ion-badge[vertical="bottom"]:empty) {
@include position($tab-md-button-icon-bottom-badge-bottom-position-empty, null, null, calc(50% + 6px));
}
// Material Design Tab Button: Icon Top Layout
// --------------------------------------------------------------
@@ -146,7 +166,7 @@
// Badge
:host(.tab-layout-label-hide) ::slotted(ion-badge),
:host(.tab-has-icon-only) ::slotted(ion-badge) {
@include position(16px, null, null, null);
@include position(16px);
}
// Icon

View File

@@ -80,3 +80,18 @@ $tab-button-md-badge-font-size: 8px;
/// @prop - Size of the empty tab button badge
$tab-button-md-badge-size-empty: 8px;
/// @prop - Badge bottom position
$tab-button-md-badge-bottom-position: calc(50% - 8px);
/// @prop - Badge bottom position when empty
$tab-button-md-badge-bottom-position-empty: calc(50% - 4px);
/// @prop - Badge bottom position when tab bottom has icon at Bottom
$tab-md-button-icon-bottom-badge-top-position: calc(50% - 4px);
/// @prop - Badge bottom position when tab bottom has icon at bottom
$tab-md-button-icon-bottom-badge-bottom-position: calc(50% + 4px);
/// @prop - Badge bottom position when empty and when tab bottom has icon at bottom
$tab-md-button-icon-bottom-badge-bottom-position-empty: calc(50% + 12px);

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB