diff --git a/core/api.txt b/core/api.txt index e4e1ca4d603..4bf94c415f6 100644 --- a/core/api.txt +++ b/core/api.txt @@ -2326,6 +2326,24 @@ ion-select,css-prop,--placeholder-opacity,md ion-select,css-prop,--ripple-color,ionic ion-select,css-prop,--ripple-color,ios ion-select,css-prop,--ripple-color,md +ion-select,css-prop,--select-text-media-border-color,ionic +ion-select,css-prop,--select-text-media-border-color,ios +ion-select,css-prop,--select-text-media-border-color,md +ion-select,css-prop,--select-text-media-border-radius,ionic +ion-select,css-prop,--select-text-media-border-radius,ios +ion-select,css-prop,--select-text-media-border-radius,md +ion-select,css-prop,--select-text-media-border-style,ionic +ion-select,css-prop,--select-text-media-border-style,ios +ion-select,css-prop,--select-text-media-border-style,md +ion-select,css-prop,--select-text-media-border-width,ionic +ion-select,css-prop,--select-text-media-border-width,ios +ion-select,css-prop,--select-text-media-border-width,md +ion-select,css-prop,--select-text-media-height,ionic +ion-select,css-prop,--select-text-media-height,ios +ion-select,css-prop,--select-text-media-height,md +ion-select,css-prop,--select-text-media-width,ionic +ion-select,css-prop,--select-text-media-width,ios +ion-select,css-prop,--select-text-media-width,md ion-select,part,bottom ion-select,part,container ion-select,part,error-text @@ -2345,6 +2363,7 @@ ion-select-modal,prop,multiple,boolean | undefined,undefined,false,false ion-select-modal,prop,options,SelectModalOption[],[],false,false ion-select-option,shadow +ion-select-option,prop,description,string | undefined,undefined,false,false ion-select-option,prop,disabled,boolean,false,false,false ion-select-option,prop,mode,"ios" | "md",undefined,false,false ion-select-option,prop,theme,"ios" | "md" | "ionic",undefined,false,false diff --git a/core/src/components.d.ts b/core/src/components.d.ts index ecbdb7475c6..86ac882a669 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -3813,6 +3813,10 @@ export namespace Components { "options": SelectModalOption[]; } interface IonSelectOption { + /** + * Text that is placed underneath the option text to provide additional details about the option. + */ + "description"?: string; /** * If `true`, the user cannot interact with the select option. This property does not apply when `interface="action-sheet"` as `ion-action-sheet` does not allow for disabled buttons. * @default false @@ -9897,6 +9901,10 @@ declare namespace LocalJSX { "options"?: SelectModalOption[]; } interface IonSelectOption { + /** + * Text that is placed underneath the option text to provide additional details about the option. + */ + "description"?: string; /** * If `true`, the user cannot interact with the select option. This property does not apply when `interface="action-sheet"` as `ion-action-sheet` does not allow for disabled buttons. * @default false @@ -11268,6 +11276,7 @@ declare namespace LocalJSX { interface IonSelectOptionAttributes { "disabled": boolean; "value": string; + "description": string; } interface IonSelectPopoverAttributes { "header": string; diff --git a/core/src/components/action-sheet/action-sheet-interface.ts b/core/src/components/action-sheet/action-sheet-interface.ts index 324835bb720..f0b4ee5971d 100644 --- a/core/src/components/action-sheet/action-sheet-interface.ts +++ b/core/src/components/action-sheet/action-sheet-interface.ts @@ -14,7 +14,7 @@ export interface ActionSheetOptions extends OverlayOptions { } export interface ActionSheetButton { - text?: string; + text?: string | HTMLElement; role?: LiteralUnion<'cancel' | 'destructive' | 'selected', string>; icon?: string; cssClass?: string | string[]; @@ -30,4 +30,7 @@ export interface ActionSheetButton { * users to dismiss the action sheet. */ disabled?: boolean; + startContent?: HTMLElement; + endContent?: HTMLElement; + description?: string; } diff --git a/core/src/components/action-sheet/action-sheet.scss b/core/src/components/action-sheet/action-sheet.common.scss similarity index 96% rename from core/src/components/action-sheet/action-sheet.scss rename to core/src/components/action-sheet/action-sheet.common.scss index 2a2f85bb456..7e857346ac9 100644 --- a/core/src/components/action-sheet/action-sheet.scss +++ b/core/src/components/action-sheet/action-sheet.common.scss @@ -233,3 +233,20 @@ } } } + +// Action Sheet: Select Option +// -------------------------------------------------- + +.action-sheet-button-label { + display: flex; + + align-items: center; +} + +.select-option-content { + flex: 1; +} + +.select-option-description { + display: block; +} diff --git a/core/src/components/action-sheet/action-sheet.ionic.scss b/core/src/components/action-sheet/action-sheet.ionic.scss new file mode 100644 index 00000000000..b2c749d4e0a --- /dev/null +++ b/core/src/components/action-sheet/action-sheet.ionic.scss @@ -0,0 +1,22 @@ +@use "../../themes/ionic/ionic.globals.scss" as globals; +@use "./action-sheet.common"; +@use "./action-sheet.md" as action-sheet-md; + +// Ionic Action Sheet +// -------------------------------------------------- + +// Action Sheet: Select Option +// -------------------------------------------------- + +.action-sheet-button-label { + gap: globals.$ion-space-300; +} + +.select-option-description { + @include globals.typography(globals.$ion-body-md-regular); + @include globals.padding(0); + + color: globals.$ion-text-subtle; + + font-size: globals.$ion-font-size-350; +} diff --git a/core/src/components/action-sheet/action-sheet.ios.scss b/core/src/components/action-sheet/action-sheet.ios.scss index fe9bba89903..94b98447981 100644 --- a/core/src/components/action-sheet/action-sheet.ios.scss +++ b/core/src/components/action-sheet/action-sheet.ios.scss @@ -1,4 +1,4 @@ -@import "./action-sheet"; +@import "./action-sheet.native"; @import "./action-sheet.ios.vars"; // iOS Action Sheet diff --git a/core/src/components/action-sheet/action-sheet.md.scss b/core/src/components/action-sheet/action-sheet.md.scss index 8e1c7f07027..e46f06085b3 100644 --- a/core/src/components/action-sheet/action-sheet.md.scss +++ b/core/src/components/action-sheet/action-sheet.md.scss @@ -1,4 +1,4 @@ -@import "./action-sheet"; +@import "./action-sheet.native"; @import "./action-sheet.md.vars"; // Material Design Action Sheet Title diff --git a/core/src/components/action-sheet/action-sheet.native.scss b/core/src/components/action-sheet/action-sheet.native.scss new file mode 100644 index 00000000000..affa6aeb126 --- /dev/null +++ b/core/src/components/action-sheet/action-sheet.native.scss @@ -0,0 +1,19 @@ +@use "../../themes/native/native.theme.default" as native; +@use "../../themes/mixins" as mixins; +@use "../../themes/functions.font" as font; +@use "./action-sheet.common"; + +// Action Sheet: Native +// -------------------------------------------------- + +.action-sheet-button-label { + gap: 12px; +} + +.select-option-description { + @include mixins.padding(5px, 0, 0, 0); + + color: native.$text-color-step-300; + + font-size: font.dynamic-font(12px); +} diff --git a/core/src/components/action-sheet/action-sheet.tsx b/core/src/components/action-sheet/action-sheet.tsx index 5d79ab90f51..4644b7f7d78 100644 --- a/core/src/components/action-sheet/action-sheet.tsx +++ b/core/src/components/action-sheet/action-sheet.tsx @@ -16,6 +16,7 @@ import { safeCall, setOverlayId, } from '@utils/overlays'; +import { renderOptionLabel } from '@utils/select-option-render'; import { getClassMap } from '@utils/theme'; import { getIonMode, getIonTheme } from '../../global/ionic-global'; @@ -37,7 +38,7 @@ import { mdLeaveAnimation } from './animations/md.leave'; styleUrls: { ios: 'action-sheet.ios.scss', md: 'action-sheet.md.scss', - ionic: 'action-sheet.md.scss', + ionic: 'action-sheet.ionic.scss', }, scoped: true, }) @@ -559,6 +560,14 @@ export class ActionSheet implements ComponentInterface, OverlayInterface { htmlAttrs['aria-checked'] = isActiveRadio ? 'true' : 'false'; } + const optionLabelOptions = { + id: buttonId, + label: b.text, + startContent: b.startContent, + endContent: b.endContent, + description: b.description, + }; + return ( diff --git a/core/src/components/alert/alert-interface.ts b/core/src/components/alert/alert-interface.ts index 8356aaede43..b79a78cba2d 100644 --- a/core/src/components/alert/alert-interface.ts +++ b/core/src/components/alert/alert-interface.ts @@ -25,7 +25,7 @@ export interface AlertInput { /** * The label text to display next to the input, if the input type is `radio` or `checkbox`. */ - label?: string; + label?: string | HTMLElement; checked?: boolean; disabled?: boolean; id?: string; @@ -35,6 +35,9 @@ export interface AlertInput { cssClass?: string | string[]; attributes?: { [key: string]: any }; tabindex?: number; + startContent?: HTMLElement; + endContent?: HTMLElement; + description?: string; } type AlertButtonOverlayHandler = boolean | void | { [key: string]: any }; diff --git a/core/src/components/alert/alert.scss b/core/src/components/alert/alert.common.scss similarity index 95% rename from core/src/components/alert/alert.scss rename to core/src/components/alert/alert.common.scss index 9948a4127a9..84e35eca5c3 100644 --- a/core/src/components/alert/alert.scss +++ b/core/src/components/alert/alert.common.scss @@ -247,3 +247,21 @@ textarea.alert-input { min-height: $alert-input-min-height; resize: none; } + +// Alert Button: Select Option +// -------------------------------------------------- + +.alert-radio-label, +.alert-checkbox-label { + display: flex; + + align-items: center; +} + +.select-option-content { + flex: 1; +} + +.select-option-description { + display: block; +} diff --git a/core/src/components/alert/alert.ionic.scss b/core/src/components/alert/alert.ionic.scss new file mode 100644 index 00000000000..3c54136b477 --- /dev/null +++ b/core/src/components/alert/alert.ionic.scss @@ -0,0 +1,23 @@ +@use "../../themes/ionic/ionic.globals.scss" as globals; +@use "./alert.common"; +@use "./alert.md" as alert-md; + +// Ionic Alert +// -------------------------------------------------- + +// Alert: Select Option +// -------------------------------------------------- + +.alert-radio-label, +.alert-checkbox-label { + gap: globals.$ion-space-300; +} + +.select-option-description { + @include globals.typography(globals.$ion-body-md-regular); + @include globals.padding(0); + + color: globals.$ion-text-subtle; + + font-size: globals.$ion-font-size-350; +} diff --git a/core/src/components/alert/alert.ios.scss b/core/src/components/alert/alert.ios.scss index 714efc03baf..2671dc0940b 100644 --- a/core/src/components/alert/alert.ios.scss +++ b/core/src/components/alert/alert.ios.scss @@ -1,4 +1,4 @@ -@import "./alert"; +@import "./alert.native"; @import "./alert.ios.vars"; // iOS Alert diff --git a/core/src/components/alert/alert.md.scss b/core/src/components/alert/alert.md.scss index 5ac468c760f..2fbd0fd8775 100644 --- a/core/src/components/alert/alert.md.scss +++ b/core/src/components/alert/alert.md.scss @@ -1,4 +1,4 @@ -@import "./alert"; +@import "./alert.native"; @import "./alert.md.vars"; // Material Design Alert diff --git a/core/src/components/alert/alert.native.scss b/core/src/components/alert/alert.native.scss new file mode 100644 index 00000000000..e2d5a87b8a5 --- /dev/null +++ b/core/src/components/alert/alert.native.scss @@ -0,0 +1,20 @@ +@use "../../themes/native/native.theme.default" as native; +@use "../../themes/mixins" as mixins; +@use "../../themes/functions.font" as font; +@use "./alert.common"; + +// Alert: Native +// -------------------------------------------------- + +.alert-radio-label, +.alert-checkbox-label { + gap: 12px; +} + +.select-option-description { + @include mixins.padding(5px, 0, 0, 0); + + color: native.$text-color-step-300; + + font-size: font.dynamic-font(12px); +} diff --git a/core/src/components/alert/alert.tsx b/core/src/components/alert/alert.tsx index e4e98b67a42..c2c44f6f2e8 100644 --- a/core/src/components/alert/alert.tsx +++ b/core/src/components/alert/alert.tsx @@ -19,6 +19,7 @@ import { setOverlayId, } from '@utils/overlays'; import { sanitizeDOMString } from '@utils/sanitization'; +import { renderOptionLabel } from '@utils/select-option-render'; import { getClassMap } from '@utils/theme'; import { config } from '../../global/config'; @@ -44,7 +45,7 @@ import { mdLeaveAnimation } from './animations/md.leave'; styleUrls: { ios: 'alert.ios.scss', md: 'alert.md.scss', - ionic: 'alert.md.scss', + ionic: 'alert.ionic.scss', }, scoped: true, }) @@ -346,6 +347,9 @@ export class Alert implements ComponentInterface, OverlayInterface { cssClass: i.cssClass ?? '', attributes: i.attributes || {}, tabindex: i.type === 'radio' && i !== focusable ? -1 : 0, + startContent: i.startContent, + endContent: i.endContent, + description: i.description, } as AlertInput) ); } @@ -569,33 +573,43 @@ export class Alert implements ComponentInterface, OverlayInterface { return (
- {inputs.map((i) => ( -
- {theme === 'md' && } - - ))} + {theme === 'md' && } + + ); + })} ); } @@ -609,32 +623,42 @@ export class Alert implements ComponentInterface, OverlayInterface { return (
- {inputs.map((i) => ( -
- - ))} + + ); + })} ); } diff --git a/core/src/components/select-modal/select-modal-interface.ts b/core/src/components/select-modal/select-modal-interface.ts index 2005400cb82..8ed968c3d12 100644 --- a/core/src/components/select-modal/select-modal-interface.ts +++ b/core/src/components/select-modal/select-modal-interface.ts @@ -1,8 +1,11 @@ export interface SelectModalOption { - text: string; + text: string | HTMLElement; value: string; disabled: boolean; checked: boolean; cssClass?: string | string[]; handler?: (value: any) => boolean | void | { [key: string]: any }; + startContent?: HTMLElement; + endContent?: HTMLElement; + description?: string; } diff --git a/core/src/components/select-modal/select-modal.common.scss b/core/src/components/select-modal/select-modal.common.scss index 683ae23faeb..3bbb48b557d 100644 --- a/core/src/components/select-modal/select-modal.common.scss +++ b/core/src/components/select-modal/select-modal.common.scss @@ -1,3 +1,19 @@ +// Select Modal +// -------------------------------------------------- + :host { height: 100%; } + +// Select Modal: Select Option +// -------------------------------------------------- + +.select-option-label { + display: flex; + + align-items: center; +} + +.select-option-description { + display: block; +} diff --git a/core/src/components/select-modal/select-modal.ionic.scss b/core/src/components/select-modal/select-modal.ionic.scss index 23d7705b660..ca137a075d3 100644 --- a/core/src/components/select-modal/select-modal.ionic.scss +++ b/core/src/components/select-modal/select-modal.ionic.scss @@ -77,3 +77,18 @@ ion-content { --background-focused-opacity: 1; } } + +// Select Modal: Select Option +// -------------------------------------------------- + +.select-option-label { + gap: globals.$ion-space-300; +} + +.select-option-description { + @include globals.typography(globals.$ion-body-md-regular); + + color: globals.$ion-text-subtle; + + font-size: globals.$ion-font-size-350; +} diff --git a/core/src/components/select-modal/select-modal.ios.scss b/core/src/components/select-modal/select-modal.ios.scss index eea8d57f0ba..abac9c8220b 100644 --- a/core/src/components/select-modal/select-modal.ios.scss +++ b/core/src/components/select-modal/select-modal.ios.scss @@ -1,4 +1,4 @@ -@import "./select-modal.common"; +@import "./select-modal.native"; @import "../item/item.ios.vars"; @import "../radio/radio.ios.vars"; diff --git a/core/src/components/select-modal/select-modal.md.scss b/core/src/components/select-modal/select-modal.md.scss index 505ea2a061c..260f6aba5be 100644 --- a/core/src/components/select-modal/select-modal.md.scss +++ b/core/src/components/select-modal/select-modal.md.scss @@ -1,4 +1,4 @@ -@import "./select-modal.common"; +@import "./select-modal.native"; @import "../../themes/mixins.scss"; @import "../item/item.md.vars"; diff --git a/core/src/components/select-modal/select-modal.native.scss b/core/src/components/select-modal/select-modal.native.scss new file mode 100644 index 00000000000..29b81819fcf --- /dev/null +++ b/core/src/components/select-modal/select-modal.native.scss @@ -0,0 +1,19 @@ +@use "../../themes/native/native.theme.default" as native; +@use "../../themes/mixins" as mixins; +@use "../../themes/functions.font" as font; +@use "./select-modal.common"; + +// Select Modal: Native +// -------------------------------------------------- + +.select-option-label { + gap: 12px; +} + +.select-option-description { + @include mixins.padding(5px, 0, 0, 0); + + color: native.$text-color-step-300; + + font-size: font.dynamic-font(12px); +} diff --git a/core/src/components/select-modal/select-modal.tsx b/core/src/components/select-modal/select-modal.tsx index c277c194da8..477e42b1c16 100644 --- a/core/src/components/select-modal/select-modal.tsx +++ b/core/src/components/select-modal/select-modal.tsx @@ -2,6 +2,7 @@ import { getIonMode } from '@global/ionic-global'; import type { ComponentInterface } from '@stencil/core'; import { Component, Element, Host, Prop, forceUpdate, h } from '@stencil/core'; import { safeCall } from '@utils/overlays'; +import { renderOptionLabel } from '@utils/select-option-render'; import { getClassMap, hostContext } from '@utils/theme'; import type { CheckboxCustomEvent } from '../checkbox/checkbox-interface'; @@ -92,66 +93,86 @@ export class SelectModal implements ComponentInterface { return ( this.callOptionHandler(ev)}> - {this.options.map((option) => ( - - this.closeModal()} - onKeyUp={(ev) => { - if (ev.key === ' ') { - /** - * Selecting a radio option with keyboard navigation, - * either through the Enter or Space keys, should - * dismiss the modal. - */ - this.closeModal(); - } + {this.options.map((option, index) => { + const optionLabelOptions = { + id: `modal-option-${index}`, + label: option.text, + startContent: option.startContent, + endContent: option.endContent, + description: option.description, + }; + + return ( + - {option.text} - - - ))} + this.closeModal()} + onKeyUp={(ev) => { + if (ev.key === ' ') { + /** + * Selecting a radio option with keyboard navigation, + * either through the Enter or Space keys, should + * dismiss the modal. + */ + this.closeModal(); + } + }} + > + {renderOptionLabel(optionLabelOptions, 'select-option-label')} + + + ); + })} ); } private renderCheckboxOptions() { - return this.options.map((option) => ( - - { - this.setChecked(ev); - this.callOptionHandler(ev); + return this.options.map((option, index) => { + const optionLabelOptions = { + id: `modal-option-${index}`, + label: option.text, + startContent: option.startContent, + endContent: option.endContent, + description: option.description, + }; + + return ( + - {option.text} - - - )); + { + this.setChecked(ev); + this.callOptionHandler(ev); + // TODO FW-4784 + forceUpdate(this); + }} + > + {renderOptionLabel(optionLabelOptions, 'select-option-label')} + + + ); + }); } render() { diff --git a/core/src/components/select-option/select-option.tsx b/core/src/components/select-option/select-option.tsx index b088f4ea72e..dea73b4fd3c 100644 --- a/core/src/components/select-option/select-option.tsx +++ b/core/src/components/select-option/select-option.tsx @@ -27,8 +27,14 @@ export class SelectOption implements ComponentInterface { */ @Prop() value?: any | null; + /** + * Text that is placed underneath the option text to provide additional details about the option. + */ + @Prop() description?: string; + render() { const theme = getIonTheme(this); + return ( + > + + + + ); } } diff --git a/core/src/components/select-popover/select-popover-interface.ts b/core/src/components/select-popover/select-popover-interface.ts index 1787234ae75..b0a30bbbf13 100644 --- a/core/src/components/select-popover/select-popover-interface.ts +++ b/core/src/components/select-popover/select-popover-interface.ts @@ -1,8 +1,11 @@ export interface SelectPopoverOption { - text: string; + text: string | HTMLElement; value: string; disabled: boolean; checked: boolean; cssClass?: string | string[]; handler?: (value: any) => boolean | void | { [key: string]: any }; + startContent?: HTMLElement; + endContent?: HTMLElement; + description?: string; } diff --git a/core/src/components/select-popover/select-popover.scss b/core/src/components/select-popover/select-popover.common.scss similarity index 59% rename from core/src/components/select-popover/select-popover.scss rename to core/src/components/select-popover/select-popover.common.scss index de7cb783300..cedc62bbf3a 100644 --- a/core/src/components/select-popover/select-popover.scss +++ b/core/src/components/select-popover/select-popover.common.scss @@ -1,5 +1,8 @@ @import "../../themes/native/native.globals"; +// Select Popover +// -------------------------------------------------- + :host ion-list { @include margin(0); } @@ -18,3 +21,16 @@ ion-label { :host { overflow-y: auto; } + +// Select Popover: Select Option +// -------------------------------------------------- + +.select-option-label { + display: flex; + + align-items: center; +} + +.select-option-description { + display: block; +} diff --git a/core/src/components/select-popover/select-popover.ionic.scss b/core/src/components/select-popover/select-popover.ionic.scss new file mode 100644 index 00000000000..c8321bb30eb --- /dev/null +++ b/core/src/components/select-popover/select-popover.ionic.scss @@ -0,0 +1,22 @@ +@use "../../themes/ionic/ionic.globals.scss" as globals; +@use "./select-popover.common"; +@use "./select-popover.md" as select-popover-md; + +// Ionic Select Popover +// -------------------------------------------------- + +// Select Modal: Select Option +// -------------------------------------------------- + +.select-option-label { + gap: globals.$ion-space-300; +} + +.select-option-description { + @include globals.typography(globals.$ion-body-md-regular); + @include globals.padding(0); + + color: globals.$ion-text-subtle; + + font-size: globals.$ion-font-size-350; +} diff --git a/core/src/components/select-popover/select-popover.ios.scss b/core/src/components/select-popover/select-popover.ios.scss index 3330a261d80..de3cfea6135 100644 --- a/core/src/components/select-popover/select-popover.ios.scss +++ b/core/src/components/select-popover/select-popover.ios.scss @@ -1,2 +1,2 @@ -@import "./select-popover"; +@import "./select-popover.native"; @import "./select-popover.ios.vars"; diff --git a/core/src/components/select-popover/select-popover.md.scss b/core/src/components/select-popover/select-popover.md.scss index 001b0123632..c7728bcaf04 100644 --- a/core/src/components/select-popover/select-popover.md.scss +++ b/core/src/components/select-popover/select-popover.md.scss @@ -1,4 +1,4 @@ -@import "./select-popover"; +@import "./select-popover.native"; @import "./select-popover.md.vars"; ion-list ion-radio::part(container) { diff --git a/core/src/components/select-popover/select-popover.native.scss b/core/src/components/select-popover/select-popover.native.scss new file mode 100644 index 00000000000..0b52fafe932 --- /dev/null +++ b/core/src/components/select-popover/select-popover.native.scss @@ -0,0 +1,19 @@ +@use "../../themes/native/native.theme.default" as native; +@use "../../themes/mixins" as mixins; +@use "../../themes/functions.font" as font; +@use "./select-popover.common"; + +// Select Popover: Native +// -------------------------------------------------- + +.select-option-label { + gap: 12px; +} + +.select-option-description { + @include mixins.padding(5px, 0, 0, 0); + + color: native.$text-color-step-300; + + font-size: font.dynamic-font(12px); +} diff --git a/core/src/components/select-popover/select-popover.tsx b/core/src/components/select-popover/select-popover.tsx index efba1c8d9c1..7b5bb28b15c 100644 --- a/core/src/components/select-popover/select-popover.tsx +++ b/core/src/components/select-popover/select-popover.tsx @@ -1,6 +1,7 @@ import type { ComponentInterface } from '@stencil/core'; import { Element, Component, Host, Prop, h, forceUpdate } from '@stencil/core'; import { safeCall } from '@utils/overlays'; +import { renderOptionLabel } from '@utils/select-option-render'; import { getClassMap } from '@utils/theme'; import { getIonTheme } from '../../global/ionic-global'; @@ -20,7 +21,7 @@ import type { SelectPopoverOption } from './select-popover-interface'; styleUrls: { ios: 'select-popover.ios.scss', md: 'select-popover.md.scss', - ionic: 'select-popover.md.scss', + ionic: 'select-popover.ionic.scss', }, scoped: true, }) @@ -119,31 +120,41 @@ export class SelectPopover implements ComponentInterface { } renderCheckboxOptions(options: SelectPopoverOption[]) { - return options.map((option) => ( - - { - this.setChecked(ev); - this.callOptionHandler(ev); + return options.map((option, index) => { + const optionLabelOptions = { + id: `popover-option-${index}`, + label: option.text, + startContent: option.startContent, + endContent: option.endContent, + description: option.description, + }; + + return ( + - {option.text} - - - )); + { + this.setChecked(ev); + this.callOptionHandler(ev); + // TODO FW-4784 + forceUpdate(this); + }} + > + {renderOptionLabel(optionLabelOptions, 'select-option-label')} + + + ); + }); } renderRadioOptions(options: SelectPopoverOption[]) { @@ -151,33 +162,43 @@ export class SelectPopover implements ComponentInterface { return ( this.callOptionHandler(ev)}> - {options.map((option) => ( - - this.dismissParentPopover()} - onKeyUp={(ev) => { - if (ev.key === ' ') { - /** - * Selecting a radio option with keyboard navigation, - * either through the Enter or Space keys, should - * dismiss the popover. - */ - this.dismissParentPopover(); - } + {options.map((option, index) => { + const optionLabelOptions = { + id: `popover-option-${index}`, + label: option.text, + startContent: option.startContent, + endContent: option.endContent, + description: option.description, + }; + + return ( + - {option.text} - - - ))} + this.dismissParentPopover()} + onKeyUp={(ev) => { + if (ev.key === ' ') { + /** + * Selecting a radio option with keyboard navigation, + * either through the Enter or Space keys, should + * dismiss the popover. + */ + this.dismissParentPopover(); + } + }} + > + {renderOptionLabel(optionLabelOptions, 'select-option-label')} + + + ); + })} ); } diff --git a/core/src/components/select/select.common.scss b/core/src/components/select/select.common.scss index 43f3d13399f..b3866c6f15b 100644 --- a/core/src/components/select/select.common.scss +++ b/core/src/components/select/select.common.scss @@ -25,6 +25,13 @@ * @prop --border-width: Width of the select border * * @prop --ripple-color: The color of the ripple effect on MD mode. + * + * @prop --select-text-media-width: The width of media (icons/images) in the select text. + * @prop --select-text-media-height: The height of media (icons/images) in the select text. + * @prop --select-text-media-border-width: The border width of media (icons/images) in the select text. + * @prop --select-text-media-border-color: The border color of media (icons/images) in the select text. + * @prop --select-text-media-border-radius: The border radius of media (icons/images) in the select text. + * @prop --select-text-media-border-style: The border style of media (icons/images) in the select text. */ --padding-top: 0px; --padding-end: 0px; @@ -36,6 +43,8 @@ --highlight-color-focused: #{ion-color(primary, base)}; --highlight-color-valid: #{ion-color(success, base)}; --highlight-color-invalid: #{ion-color(danger, base)}; + --select-text-media-height: 1.5em; + --select-text-media-width: 1.5em; /** * This is a private API that is used to switch @@ -152,6 +161,21 @@ button { overflow: hidden; } +.select-text img, +.select-text ion-img, +.select-text ion-icon, +.select-text ion-thumbnail, +.select-text ion-avatar { + @include mixins.border-radius(var(--select-text-media-border-radius)); + + width: var(--select-text-media-width); + height: var(--select-text-media-height); + + border-width: var(--select-text-media-border-width); + border-style: var(--select-text-media-border-style); + border-color: var(--select-text-media-border-color); +} + // Select Wrapper // -------------------------------------------------- diff --git a/core/src/components/select/select.ionic.scss b/core/src/components/select/select.ionic.scss index 2ba5f722406..5f0bf27977f 100644 --- a/core/src/components/select/select.ionic.scss +++ b/core/src/components/select/select.ionic.scss @@ -50,6 +50,15 @@ color: globals.$ion-text-default; } +/** + * If the select text contains rich content, we want to add some + * spacing between the items without changing the display to prevent + * losing the ellipsis behavior. + */ +.select-text > * { + margin-inline-start: globals.$ion-space-200; +} + // Select Label // ---------------------------------------------------------------- diff --git a/core/src/components/select/select.native.scss b/core/src/components/select/select.native.scss index 876fcb1579f..262b6efdbee 100644 --- a/core/src/components/select/select.native.scss +++ b/core/src/components/select/select.native.scss @@ -88,6 +88,15 @@ min-width: 16px; } +/** + * If the select text contains rich content, we want to add some + * spacing between the items without changing the display to prevent + * losing the ellipsis behavior. + */ +.select-text > * { + margin-inline-start: 8px; +} + // Select Label // ---------------------------------------------------------------- diff --git a/core/src/components/select/select.tsx b/core/src/components/select/select.tsx index 8071434ba8c..41836a95f96 100644 --- a/core/src/components/select/select.tsx +++ b/core/src/components/select/select.tsx @@ -1,6 +1,7 @@ import caretDownRegular from '@phosphor-icons/core/assets/regular/caret-down.svg'; import type { ComponentInterface, EventEmitter } from '@stencil/core'; import { Build, Component, Element, Event, Host, Method, Prop, State, Watch, h, forceUpdate } from '@stencil/core'; +import { ENABLE_HTML_CONTENT_DEFAULT } from '@utils/config'; import type { NotchController } from '@utils/forms'; import { compareOptions, createNotchController, isOptionSelected, checkInvalidState } from '@utils/forms'; import { focusVisibleElement, renderHiddenInput, inheritAttributes } from '@utils/helpers'; @@ -9,6 +10,7 @@ import { printIonWarning } from '@utils/logging'; import { actionSheetController, alertController, popoverController, modalController } from '@utils/overlays'; import type { OverlaySelect } from '@utils/overlays-interface'; import { isRTL } from '@utils/rtl'; +import { sanitizeDOMString } from '@utils/sanitization'; import { createColorClasses, hostContext } from '@utils/theme'; import { watchForOptions } from '@utils/watch-options'; import { caretDownSharp, chevronExpand } from 'ionicons/icons'; @@ -72,8 +74,8 @@ export class Select implements ComponentInterface { private nativeWrapperEl: HTMLElement | undefined; private notchSpacerEl: HTMLElement | undefined; private validationObserver?: MutationObserver; - private notchController?: NotchController; + private customHTMLEnabled = config.get('innerHTMLTemplatesEnabled', ENABLE_HTML_CONTENT_DEFAULT); @Element() el!: HTMLIonSelectElement; @@ -576,10 +578,15 @@ export class Select implements ComponentInterface { .join(' '); const optClass = `${OPTION_CLASS} ${copyClasses}`; const isSelected = isOptionSelected(selectValue, value, this.compareWith); + const text = this.customHTMLEnabled ? getOptionContent(option) : option.textContent; + const startContent = this.customHTMLEnabled + ? (getOptionContent(option, 'start') as HTMLElement | null) + : undefined; + const endContent = this.customHTMLEnabled ? (getOptionContent(option, 'end') as HTMLElement | null) : undefined; return { role: isSelected ? 'selected' : '', - text: option.textContent, + text: text ?? '', cssClass: optClass, handler: () => { this.setValue(value); @@ -588,6 +595,9 @@ export class Select implements ComponentInterface { 'aria-checked': isSelected ? 'true' : 'false', role: 'radio', }, + startContent: startContent ?? undefined, + endContent: endContent ?? undefined, + description: option.description, } as ActionSheetButton; }); @@ -616,14 +626,22 @@ export class Select implements ComponentInterface { .filter((cls) => cls !== 'hydrated') .join(' '); const optClass = `${OPTION_CLASS} ${copyClasses}`; + const label = this.customHTMLEnabled ? getOptionContent(option) : option.textContent; + const startContent = this.customHTMLEnabled + ? (getOptionContent(option, 'start') as HTMLElement | null) + : undefined; + const endContent = this.customHTMLEnabled ? (getOptionContent(option, 'end') as HTMLElement | null) : undefined; return { type: inputType, cssClass: optClass, - label: option.textContent || '', + label: label ?? '', value, checked: isOptionSelected(selectValue, value, this.compareWith), disabled: option.disabled, + startContent: startContent ?? undefined, + endContent: endContent ?? undefined, + description: option.description, }; }); @@ -639,9 +657,14 @@ export class Select implements ComponentInterface { .filter((cls) => cls !== 'hydrated') .join(' '); const optClass = `${OPTION_CLASS} ${copyClasses}`; + const text = this.customHTMLEnabled ? getOptionContent(option) : option.textContent; + const startContent = this.customHTMLEnabled + ? (getOptionContent(option, 'start') as HTMLElement | null) + : undefined; + const endContent = this.customHTMLEnabled ? (getOptionContent(option, 'end') as HTMLElement | null) : undefined; return { - text: option.textContent || '', + text: text ?? '', cssClass: optClass, value, checked: isOptionSelected(selectValue, value, this.compareWith), @@ -652,6 +675,9 @@ export class Select implements ComponentInterface { this.close(); } }, + startContent: startContent ?? undefined, + endContent: endContent ?? undefined, + description: option.description, }; }); @@ -879,12 +905,18 @@ export class Select implements ComponentInterface { return; } - private getText(): string { + /** + * Returns the text to display in the select based on the selected value. + * + * @param useHTML If `true`, the returned text will include any custom HTML content from the selected option. If `false`, the returned text will be plain text without any HTML. Defaults to `false`. + * @returns The text to display in the select, either with or without HTML based on the `useHTML` parameter. + */ + private getText(useHTML = false): string { const selectedText = this.selectedText; if (selectedText != null && selectedText !== '') { return selectedText; } - return generateText(this.childOpts, this.value, this.compareWith); + return generateText(this.childOpts, this.value, this.compareWith, useHTML); } private setFocus() { @@ -1069,7 +1101,7 @@ export class Select implements ComponentInterface { private renderSelectText() { const { placeholder } = this; - const displayValue = this.getText(); + const displayValue = this.getText(true); let addPlaceholderClass = false; let selectText = displayValue; @@ -1085,6 +1117,10 @@ export class Select implements ComponentInterface { const textPart = addPlaceholderClass ? 'placeholder' : 'text'; + if (this.customHTMLEnabled) { + return ; + } + return (