diff --git a/core/api.txt b/core/api.txt index b70c7e2f0ca..b0ba319a792 100644 --- a/core/api.txt +++ b/core/api.txt @@ -313,10 +313,12 @@ ion-backdrop,event,ionBackdropTap,void,true ion-badge,shadow ion-badge,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record | undefined,undefined,false,true +ion-badge,prop,hintPosition,"bottom-right" | "top-right",'top-right',false,false 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,useAsHint,boolean,false,false,false ion-badge,css-prop,--background,ionic ion-badge,css-prop,--background,ios ion-badge,css-prop,--background,md @@ -2181,6 +2183,14 @@ ion-split-pane,css-prop,--side-width,ionic ion-split-pane,css-prop,--side-width,ios ion-split-pane,css-prop,--side-width,md +ion-status-hint,shadow +ion-status-hint,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record | undefined,undefined,false,true +ion-status-hint,prop,mode,"ios" | "md",undefined,false,false +ion-status-hint,prop,position,"bottom-right" | "static" | "top-right",'static',false,false +ion-status-hint,prop,shape,"rectangular" | "round" | "soft" | undefined,undefined,false,false +ion-status-hint,prop,size,"large" | "medium" | "small" | undefined,undefined,false,false +ion-status-hint,prop,theme,"ios" | "md" | "ionic",undefined,false,false + ion-tab,shadow ion-tab,prop,component,Function | HTMLElement | null | string | undefined,undefined,false,false ion-tab,prop,mode,"ios" | "md",undefined,false,false diff --git a/core/src/components.d.ts b/core/src/components.d.ts index fa663fea9fe..5fcf6acf220 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -424,6 +424,7 @@ export namespace Components { * The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). */ "color"?: Color; + "hintPosition": 'top-right' | 'bottom-right'; /** * The mode determines the platform behaviors of the component. */ @@ -440,6 +441,7 @@ export namespace Components { * The theme determines the visual appearance of the component. */ "theme"?: "ios" | "md" | "ionic"; + "useAsHint": boolean; } interface IonBreadcrumb { /** @@ -3425,6 +3427,26 @@ export namespace Components { */ "when": string | boolean; } + interface IonStatusHint { + "color"?: Color; + /** + * The mode determines the platform behaviors of the component. + */ + "mode"?: "ios" | "md"; + "position": 'top-right' | 'bottom-right' | 'static'; + /** + * Set to `"soft"` for an hint with slightly rounded corners, `"round"` for an hint with fully rounded corners, or `"rectangular"` for an hint without rounded corners. Defaults to `"round"`. + */ + "shape"?: 'soft' | 'round' | 'rectangular'; + /** + * Set to `"small"` for a compact size. Set to `"medium"` for the default height and width. Set to `"large"` for a larger size. Defaults to `"small"`. + */ + "size"?: 'small' | 'medium' | 'large'; + /** + * The theme determines the visual appearance of the component. + */ + "theme"?: "ios" | "md" | "ionic"; + } interface IonTab { "active": boolean; /** @@ -5151,6 +5173,12 @@ declare global { prototype: HTMLIonSplitPaneElement; new (): HTMLIonSplitPaneElement; }; + interface HTMLIonStatusHintElement extends Components.IonStatusHint, HTMLStencilElement { + } + var HTMLIonStatusHintElement: { + prototype: HTMLIonStatusHintElement; + new (): HTMLIonStatusHintElement; + }; interface HTMLIonTabElement extends Components.IonTab, HTMLStencilElement { } var HTMLIonTabElement: { @@ -5395,6 +5423,7 @@ declare global { "ion-skeleton-text": HTMLIonSkeletonTextElement; "ion-spinner": HTMLIonSpinnerElement; "ion-split-pane": HTMLIonSplitPaneElement; + "ion-status-hint": HTMLIonStatusHintElement; "ion-tab": HTMLIonTabElement; "ion-tab-bar": HTMLIonTabBarElement; "ion-tab-button": HTMLIonTabButtonElement; @@ -5784,6 +5813,7 @@ declare namespace LocalJSX { * The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). */ "color"?: Color; + "hintPosition"?: 'top-right' | 'bottom-right'; /** * The mode determines the platform behaviors of the component. */ @@ -5800,6 +5830,7 @@ declare namespace LocalJSX { * The theme determines the visual appearance of the component. */ "theme"?: "ios" | "md" | "ionic"; + "useAsHint"?: boolean; } interface IonBreadcrumb { /** @@ -8868,6 +8899,26 @@ declare namespace LocalJSX { */ "when"?: string | boolean; } + interface IonStatusHint { + "color"?: Color; + /** + * The mode determines the platform behaviors of the component. + */ + "mode"?: "ios" | "md"; + "position"?: 'top-right' | 'bottom-right' | 'static'; + /** + * Set to `"soft"` for an hint with slightly rounded corners, `"round"` for an hint with fully rounded corners, or `"rectangular"` for an hint without rounded corners. Defaults to `"round"`. + */ + "shape"?: 'soft' | 'round' | 'rectangular'; + /** + * Set to `"small"` for a compact size. Set to `"medium"` for the default height and width. Set to `"large"` for a larger size. Defaults to `"small"`. + */ + "size"?: 'small' | 'medium' | 'large'; + /** + * The theme determines the visual appearance of the component. + */ + "theme"?: "ios" | "md" | "ionic"; + } interface IonTab { "active"?: boolean; /** @@ -9464,6 +9515,7 @@ declare namespace LocalJSX { "ion-skeleton-text": IonSkeletonText; "ion-spinner": IonSpinner; "ion-split-pane": IonSplitPane; + "ion-status-hint": IonStatusHint; "ion-tab": IonTab; "ion-tab-bar": IonTabBar; "ion-tab-button": IonTabButton; @@ -9566,6 +9618,7 @@ declare module "@stencil/core" { "ion-skeleton-text": LocalJSX.IonSkeletonText & JSXBase.HTMLAttributes; "ion-spinner": LocalJSX.IonSpinner & JSXBase.HTMLAttributes; "ion-split-pane": LocalJSX.IonSplitPane & JSXBase.HTMLAttributes; + "ion-status-hint": LocalJSX.IonStatusHint & JSXBase.HTMLAttributes; "ion-tab": LocalJSX.IonTab & JSXBase.HTMLAttributes; "ion-tab-bar": LocalJSX.IonTabBar & JSXBase.HTMLAttributes; "ion-tab-button": LocalJSX.IonTabButton & JSXBase.HTMLAttributes; diff --git a/core/src/components/badge/badge.common.scss b/core/src/components/badge/badge.common.scss index b690922a616..e4e87b13024 100644 --- a/core/src/components/badge/badge.common.scss +++ b/core/src/components/badge/badge.common.scss @@ -48,6 +48,13 @@ color: #{color.current-color(contrast)}; } -:host(:empty) { +:host(:empty):not(.badge-hint) { display: none; } + +ion-avatar ::slotted(ion-badge) { + position: absolute; + right: 0; + + font-size: var(--ion-font-size-275, 0.6875rem); +} \ No newline at end of file diff --git a/core/src/components/badge/badge.tsx b/core/src/components/badge/badge.tsx index e15edca816e..4dfb49e1757 100644 --- a/core/src/components/badge/badge.tsx +++ b/core/src/components/badge/badge.tsx @@ -47,6 +47,10 @@ export class Badge implements ComponentInterface { */ @Prop() size?: 'xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge'; + @Prop() useAsHint: boolean = false; + + @Prop() hintPosition: 'top-right' | 'bottom-right' = 'top-right'; + private getShape(): string | undefined { const theme = getIonTheme(this); const { shape } = this; @@ -63,11 +67,12 @@ export class Badge implements ComponentInterface { return shape; } + private getSize(): string | undefined { const theme = getIonTheme(this); - const { size } = this; + const { size, useAsHint } = this; - // TODO(ROU-10747): Remove theme check when sizes are defined for all themes. + // TODO: Remove theme check when sizes are defined for all themes. if (theme !== 'ionic') { return undefined; } @@ -76,9 +81,14 @@ export class Badge implements ComponentInterface { return 'small'; } + if (useAsHint && size && !['small', 'medium', 'large'].includes(size)) { + return 'small'; + } + return size; } + render() { const shape = this.getShape(); const size = this.getSize(); @@ -89,6 +99,8 @@ export class Badge implements ComponentInterface { [theme]: true, [`badge-${shape}`]: shape !== undefined, [`badge-${size}`]: size !== undefined, + ['badge-hint']: this.useAsHint, + [`badge-hint-${this.hintPosition}`]: this.useAsHint, })} > diff --git a/core/src/components/button/button.common.scss b/core/src/components/button/button.common.scss index f28f8f4982d..48c87488de2 100644 --- a/core/src/components/button/button.common.scss +++ b/core/src/components/button/button.common.scss @@ -304,4 +304,4 @@ ion-ripple-effect { :host(.button-solid.in-toolbar:not(.ion-color):not(.in-toolbar-color)) .button-native { background: #{var(--ion-toolbar-color, var(--background))}; color: #{var(--ion-toolbar-background, var(--color))}; -} +} \ No newline at end of file diff --git a/core/src/components/status-hint/status-hint.ionic.scss b/core/src/components/status-hint/status-hint.ionic.scss new file mode 100644 index 00000000000..44c7a5973a9 --- /dev/null +++ b/core/src/components/status-hint/status-hint.ionic.scss @@ -0,0 +1,91 @@ +@use "../../themes/ionic/ionic.globals.scss" as globals; +@use "../../themes/functions.color" as color; + +// Ionic Badge +// -------------------------------------------------- + +:host { + --background: #{globals.ion-color(primary, base)}; + --color: #{globals.ion-color(primary, contrast)}; + + display: inline-flex; + + position: static; + + align-items: center; + justify-content: center; + + font-size: globals.$ion-font-size-275; + + overflow: hidden; +} + +:host(.ion-color) { + background: #{color.current-color(base)}; + color: #{color.current-color(contrast)}; +} + +// Hint Positions +// -------------------------------------------------- +:host(.hint-top-right), +:host(.hint-bottom-right) { + position: absolute; + right: 0; + + border-width: globals.$ion-border-size-025; + border-style: globals.$ion-border-style-solid; + border-color: #fff; +} + +:host(.hint-top-right) { + top: 0; +} + +:host(.hint-bottom-right) { + bottom: 0; +} + +// Hint Shapes +// -------------------------------------------------- + +/* Soft */ +:host(.hint-soft) { + @include globals.border-radius(globals.$ion-border-radius-200); +} + +:host(.hint-small.hint-soft) { + @include globals.border-radius(globals.$ion-border-radius-100); +} + +/* Round */ +:host(.hint-round) { + @include globals.border-radius(globals.$ion-border-radius-full); +} + +/* Rectangular */ +:host(.hint-rectangular) { + @include globals.border-radius(globals.$ion-border-radius-0); +} + +// Hint Sizes +// -------------------------------------------------- + +/* Small status-hint */ +:host(.hint-small) { + width: globals.$ion-scale-200; + height: globals.$ion-scale-200; + + transform: translateX(-50%); +} + +/* Small status-hint */ +:host(.hint-medium) { + width: globals.$ion-scale-300; + height: globals.$ion-scale-300; +} + +/* Large status-hint */ +:host(.hint-large) { + width: globals.$ion-scale-400; + height: globals.$ion-scale-400; +} \ No newline at end of file diff --git a/core/src/components/status-hint/status-hint.tsx b/core/src/components/status-hint/status-hint.tsx new file mode 100644 index 00000000000..e5f7ff72f87 --- /dev/null +++ b/core/src/components/status-hint/status-hint.tsx @@ -0,0 +1,89 @@ +import type { ComponentInterface } from '@stencil/core'; +import { Component, Element, Host, Prop, h } from '@stencil/core'; +import { createColorClasses } from '@utils/theme'; + +import { getIonTheme } from '../../global/ionic-global'; +import type { Color } from '../../interface'; + +/** + * @virtualProp {"ios" | "md"} mode - The mode determines the platform behaviors of the component. + * @virtualProp {"ios" | "md" | "ionic"} theme - The theme determines the visual appearance of the component. + */ +@Component({ + tag: 'ion-status-hint', + styleUrls: { + ionic: 'status-hint.ionic.scss', + }, + shadow: true, +}) +export class StatusHint implements ComponentInterface { + @Element() el!: HTMLElement; + + @Prop({ reflect: true }) color?: Color; + + /** + * Set to `"small"` for a compact size. + * Set to `"medium"` for the default height and width. + * Set to `"large"` for a larger size. + * + * Defaults to `"small"`. + */ + @Prop() size?: 'small' | 'medium' | 'large'; + + /** + * Set to `"soft"` for an hint with slightly rounded corners, + * `"round"` for an hint with fully rounded corners, or `"rectangular"` + * for an hint without rounded corners. + * + * Defaults to `"round"`. + */ + @Prop() shape?: 'soft' | 'round' | 'rectangular'; + + @Prop() position: 'top-right' | 'bottom-right' | 'static' = 'static'; + + private getSize(): string | undefined { + const { size } = this; + + if (size === undefined) { + return 'large'; + } + + return size; + } + + private getShape(): string | undefined { + const { shape } = this; + + if (shape === undefined) { + return 'round'; + } + + return shape; + } + + private getHintPosition(): string { + return this.position; + } + + render() { + const theme = getIonTheme(this); + const size = this.getSize(); + const shape = this.getShape(); + const position = this.getHintPosition(); + + return ( + + + + + ); + } +} diff --git a/core/src/components/status-hint/test/index.html b/core/src/components/status-hint/test/index.html new file mode 100644 index 00000000000..d50696ab3e3 --- /dev/null +++ b/core/src/components/status-hint/test/index.html @@ -0,0 +1,64 @@ + + + + + Status Hint - Basic + + + + + + + + + + + + + Status Hint - Basic + + + + + + + Isolated Status Hint + + + Small + + + + Medium + + + + Large (default) + + + + + + + Status Hint in Avatar + + + Top + + 1 + + + + Bottom + + 1 + + + + + + + diff --git a/packages/angular/src/directives/proxies-list.ts b/packages/angular/src/directives/proxies-list.ts index b61d09669ca..4b9d51b0139 100644 --- a/packages/angular/src/directives/proxies-list.ts +++ b/packages/angular/src/directives/proxies-list.ts @@ -77,6 +77,7 @@ export const DIRECTIVES = [ d.IonSkeletonText, d.IonSpinner, d.IonSplitPane, + d.IonStatusHint, d.IonTab, d.IonTabBar, d.IonTabButton, diff --git a/packages/angular/src/directives/proxies.ts b/packages/angular/src/directives/proxies.ts index f187edb0741..635650dcbd3 100644 --- a/packages/angular/src/directives/proxies.ts +++ b/packages/angular/src/directives/proxies.ts @@ -261,14 +261,14 @@ export declare interface IonBackdrop extends Components.IonBackdrop { @ProxyCmp({ - inputs: ['color', 'mode', 'shape', 'size', 'theme'] + inputs: ['color', 'hintPosition', 'mode', 'shape', 'size', 'theme', 'useAsHint'] }) @Component({ selector: 'ion-badge', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'mode', 'shape', 'size', 'theme'], + inputs: ['color', 'hintPosition', 'mode', 'shape', 'size', 'theme', 'useAsHint'], }) export class IonBadge { protected el: HTMLElement; @@ -2230,6 +2230,28 @@ export declare interface IonSplitPane extends Components.IonSplitPane { } +@ProxyCmp({ + inputs: ['color', 'mode', 'position', 'shape', 'size', 'theme'] +}) +@Component({ + selector: 'ion-status-hint', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: ['color', 'mode', 'position', 'shape', 'size', 'theme'], +}) +export class IonStatusHint { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { + c.detach(); + this.el = r.nativeElement; + } +} + + +export declare interface IonStatusHint extends Components.IonStatusHint {} + + @ProxyCmp({ inputs: ['component', 'mode', 'tab', 'theme'], methods: ['setActive'] diff --git a/packages/angular/standalone/src/directives/proxies.ts b/packages/angular/standalone/src/directives/proxies.ts index 8a144f3aeb8..3b3d0ec76aa 100644 --- a/packages/angular/standalone/src/directives/proxies.ts +++ b/packages/angular/standalone/src/directives/proxies.ts @@ -72,6 +72,7 @@ import { defineCustomElement as defineIonSelectOption } from '@ionic/core/compon import { defineCustomElement as defineIonSkeletonText } from '@ionic/core/components/ion-skeleton-text.js'; import { defineCustomElement as defineIonSpinner } from '@ionic/core/components/ion-spinner.js'; import { defineCustomElement as defineIonSplitPane } from '@ionic/core/components/ion-split-pane.js'; +import { defineCustomElement as defineIonStatusHint } from '@ionic/core/components/ion-status-hint.js'; import { defineCustomElement as defineIonTab } from '@ionic/core/components/ion-tab.js'; import { defineCustomElement as defineIonTabBar } from '@ionic/core/components/ion-tab-bar.js'; import { defineCustomElement as defineIonTabButton } from '@ionic/core/components/ion-tab-button.js'; @@ -349,14 +350,14 @@ export declare interface IonBackdrop extends Components.IonBackdrop { @ProxyCmp({ defineCustomElementFn: defineIonBadge, - inputs: ['color', 'mode', 'shape', 'size', 'theme'] + inputs: ['color', 'hintPosition', 'mode', 'shape', 'size', 'theme', 'useAsHint'] }) @Component({ selector: 'ion-badge', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'mode', 'shape', 'size', 'theme'], + inputs: ['color', 'hintPosition', 'mode', 'shape', 'size', 'theme', 'useAsHint'], standalone: true }) export class IonBadge { @@ -2030,6 +2031,30 @@ export declare interface IonSplitPane extends Components.IonSplitPane { } +@ProxyCmp({ + defineCustomElementFn: defineIonStatusHint, + inputs: ['color', 'mode', 'position', 'shape', 'size', 'theme'] +}) +@Component({ + selector: 'ion-status-hint', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: ['color', 'mode', 'position', 'shape', 'size', 'theme'], + standalone: true +}) +export class IonStatusHint { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { + c.detach(); + this.el = r.nativeElement; + } +} + + +export declare interface IonStatusHint extends Components.IonStatusHint {} + + @ProxyCmp({ defineCustomElementFn: defineIonTab, inputs: ['component', 'mode', 'tab', 'theme'], diff --git a/packages/react/src/components/proxies.ts b/packages/react/src/components/proxies.ts index a9f1416ee22..888875850bf 100644 --- a/packages/react/src/components/proxies.ts +++ b/packages/react/src/components/proxies.ts @@ -69,6 +69,7 @@ import { defineCustomElement as defineIonSelectOption } from '@ionic/core/compon import { defineCustomElement as defineIonSkeletonText } from '@ionic/core/components/ion-skeleton-text.js'; import { defineCustomElement as defineIonSpinner } from '@ionic/core/components/ion-spinner.js'; import { defineCustomElement as defineIonSplitPane } from '@ionic/core/components/ion-split-pane.js'; +import { defineCustomElement as defineIonStatusHint } from '@ionic/core/components/ion-status-hint.js'; import { defineCustomElement as defineIonTab } from '@ionic/core/components/ion-tab.js'; import { defineCustomElement as defineIonText } from '@ionic/core/components/ion-text.js'; import { defineCustomElement as defineIonTextarea } from '@ionic/core/components/ion-textarea.js'; @@ -141,6 +142,7 @@ export const IonSelectOption = /*@__PURE__*/createReactComponent('ion-skeleton-text', undefined, undefined, defineIonSkeletonText); export const IonSpinner = /*@__PURE__*/createReactComponent('ion-spinner', undefined, undefined, defineIonSpinner); export const IonSplitPane = /*@__PURE__*/createReactComponent('ion-split-pane', undefined, undefined, defineIonSplitPane); +export const IonStatusHint = /*@__PURE__*/createReactComponent('ion-status-hint', undefined, undefined, defineIonStatusHint); export const IonTab = /*@__PURE__*/createReactComponent('ion-tab', undefined, undefined, defineIonTab); export const IonText = /*@__PURE__*/createReactComponent('ion-text', undefined, undefined, defineIonText); export const IonTextarea = /*@__PURE__*/createReactComponent('ion-textarea', undefined, undefined, defineIonTextarea); diff --git a/packages/vue/src/proxies.ts b/packages/vue/src/proxies.ts index 86357806b4d..db7d0760e42 100644 --- a/packages/vue/src/proxies.ts +++ b/packages/vue/src/proxies.ts @@ -75,6 +75,7 @@ import { defineCustomElement as defineIonSelectOption } from '@ionic/core/compon import { defineCustomElement as defineIonSkeletonText } from '@ionic/core/components/ion-skeleton-text.js'; import { defineCustomElement as defineIonSpinner } from '@ionic/core/components/ion-spinner.js'; import { defineCustomElement as defineIonSplitPane } from '@ionic/core/components/ion-split-pane.js'; +import { defineCustomElement as defineIonStatusHint } from '@ionic/core/components/ion-status-hint.js'; import { defineCustomElement as defineIonTab } from '@ionic/core/components/ion-tab.js'; import { defineCustomElement as defineIonText } from '@ionic/core/components/ion-text.js'; import { defineCustomElement as defineIonTextarea } from '@ionic/core/components/ion-textarea.js'; @@ -124,7 +125,9 @@ export const IonBackdrop = /*@__PURE__*/ defineContainer('ion-b export const IonBadge = /*@__PURE__*/ defineContainer('ion-badge', defineIonBadge, [ 'color', 'shape', - 'size' + 'size', + 'useAsHint', + 'hintPosition' ]); @@ -848,6 +851,14 @@ export const IonSplitPane = /*@__PURE__*/ defineContainer('ion ]); +export const IonStatusHint = /*@__PURE__*/ defineContainer('ion-status-hint', defineIonStatusHint, [ + 'color', + 'size', + 'shape', + 'position' +]); + + export const IonTab = /*@__PURE__*/ defineContainer('ion-tab', defineIonTab, [ 'active', 'delegate',