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>
@@ -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
|
||||
|
||||
8
core/src/components.d.ts
vendored
@@ -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 {
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
@include border-radius(var(--border-radius));
|
||||
|
||||
display: block;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
::slotted(ion-img),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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%);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
24
core/src/components/badge/test/hint/badge.e2e.ts
Normal 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`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
104
core/src/components/badge/test/hint/index.html
Normal 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>
|
||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
]);
|
||||
|
||||
|
||||
|
||||