diff --git a/core/src/components/select/select.ios.scss b/core/src/components/select/select.ios.scss index cc41c8fd1ca..0f8232a59d7 100644 --- a/core/src/components/select/select.ios.scss +++ b/core/src/components/select/select.ios.scss @@ -21,11 +21,11 @@ color: #{$text-color-step-350}; } -// Select Native Wrapper +// Select Inner Wrapper // ---------------------------------------------------------------- -:host(.select-label-placement-stacked) .native-wrapper, -:host(.select-label-placement-floating) .native-wrapper { +:host(.select-label-placement-stacked) .select-wrapper-inner, +:host(.select-label-placement-floating) .select-wrapper-inner { width: calc(100% - $select-ios-icon-size - $select-icon-margin-start); } diff --git a/core/src/components/select/select.md.outline.scss b/core/src/components/select/select.md.outline.scss index 705448e1a01..d22c4106c22 100644 --- a/core/src/components/select/select.md.outline.scss +++ b/core/src/components/select/select.md.outline.scss @@ -102,10 +102,7 @@ /** * This makes the label sit above the select. */ -:host(.select-expanded.select-fill-outline.select-label-placement-floating) .label-text-wrapper, -:host(.ion-focused.select-fill-outline.select-label-placement-floating) .label-text-wrapper, -:host(.has-value.select-fill-outline.select-label-placement-floating) .label-text-wrapper, -:host(.select-fill-outline.select-label-placement-stacked) .label-text-wrapper { +:host(.label-floating.select-fill-outline) .label-text-wrapper { @include transform(translateY(-32%), scale(#{$form-control-label-stacked-scale})); @include margin(0); @@ -252,9 +249,6 @@ * the floating/stacked label. We simulate this "cut out" * by removing the top border from the notch fragment. */ -:host(.select-expanded.select-fill-outline.select-label-placement-floating) .select-outline-notch, -:host(.ion-focused.select-fill-outline.select-label-placement-floating) .select-outline-notch, -:host(.has-value.select-fill-outline.select-label-placement-floating) .select-outline-notch, -:host(.select-fill-outline.select-label-placement-stacked) .select-outline-notch { +:host(.label-floating.select-fill-outline) .select-outline-notch { border-top: none; } diff --git a/core/src/components/select/select.md.scss b/core/src/components/select/select.md.scss index a9036c1d0a1..47fccd28588 100644 --- a/core/src/components/select/select.md.scss +++ b/core/src/components/select/select.md.scss @@ -138,11 +138,11 @@ --border-radius: 16px; } -// Select Native Wrapper +// Select Inner Wrapper // ---------------------------------------------------------------- -:host(.select-label-placement-stacked) .native-wrapper, -:host(.select-label-placement-floating) .native-wrapper { +:host(.select-label-placement-stacked) .select-wrapper-inner, +:host(.select-label-placement-floating) .select-wrapper-inner { width: calc(100% - $select-md-icon-size - $select-icon-margin-start); } diff --git a/core/src/components/select/select.md.solid.scss b/core/src/components/select/select.md.solid.scss index 0475b30be4c..7d48ddb10be 100644 --- a/core/src/components/select/select.md.solid.scss +++ b/core/src/components/select/select.md.solid.scss @@ -68,10 +68,7 @@ // Select Label // ---------------------------------------------------------------- -:host(.select-fill-solid.select-label-placement-stacked) .label-text-wrapper, -:host(.select-expanded.select-fill-solid.select-label-placement-floating) .label-text-wrapper, -:host(.ion-focused.select-fill-solid.select-label-placement-floating) .label-text-wrapper, -:host(.has-value.select-fill-solid.select-label-placement-floating) .label-text-wrapper { +:host(.label-floating.select-fill-solid) .label-text-wrapper { /** * Label text should not extend * beyond the bounds of the select. diff --git a/core/src/components/select/select.scss b/core/src/components/select/select.scss index 020dd51231e..49127594793 100644 --- a/core/src/components/select/select.scss +++ b/core/src/components/select/select.scss @@ -157,6 +157,13 @@ button { @include margin(0, 0, 0, $select-icon-margin-start); position: relative; + + /** + * Prevent the icon from shrinking when the label and/or + * selected item text is long enough to fill the rest of + * the container. + */ + flex-shrink: 0; } /** @@ -259,6 +266,25 @@ button { transition: opacity 150ms cubic-bezier(0.4, 0, 0.2, 1); } +.select-wrapper-inner { + display: flex; + + align-items: center; + + overflow: hidden; +} + +:host(.select-label-placement-stacked) .select-wrapper-inner, +:host(.select-label-placement-floating) .select-wrapper-inner { + /** + * When using a stacked/floating label, the inner wrapper is + * stacked vertically under the label container. This line + * ensures that the inner wrapper fills all the remaining height + * of the component. + */ + flex-grow: 1; +} + // Select Highlight // ---------------------------------------------------------------- @@ -519,11 +545,23 @@ button { * The placeholder should be hidden when the label * is on top of the select. This prevents the label * from overlapping any placeholder value. + * + * TODO(FW-5592): Remove :not(.label-floating) piece */ -:host(.select-label-placement-floating) .native-wrapper .select-placeholder { +:host(.select-label-placement-floating:not(.label-floating)) .native-wrapper .select-placeholder { opacity: 0; } +/** + * We don't use .label-floating here because that would + * also include the case where the label is floating due + * to content in the start/end slot. We want the opacity + * to remain at the default in this case, since the select + * isn't being actively interacted with. + * + * TODO(FW-5592): Change entire selector to: + * :host(.label-floating.select-label-placement-floating) .native-wrapper .select-placeholder + */ :host(.select-expanded.select-label-placement-floating) .native-wrapper .select-placeholder, :host(.ion-focused.select-label-placement-floating) .native-wrapper .select-placeholder, :host(.has-value.select-label-placement-floating) .native-wrapper .select-placeholder { @@ -533,10 +571,7 @@ button { /** * This makes the label sit above the input. */ -:host(.select-label-placement-stacked) .label-text-wrapper, -:host(.select-expanded.select-label-placement-floating) .label-text-wrapper, -:host(.ion-focused.select-label-placement-floating) .label-text-wrapper, -:host(.has-value.select-label-placement-floating) .label-text-wrapper { +:host(.label-floating) .label-text-wrapper { @include transform(translateY(50%), scale(#{$form-control-label-stacked-scale})); /** @@ -545,3 +580,23 @@ button { */ max-width: calc(100% / #{$form-control-label-stacked-scale}); } + +// Start/End Slots +// ---------------------------------------------------------------- + +::slotted([slot="start"]), ::slotted([slot="end"]) { + /** + * Prevent the slots from shrinking when the label and/or + * selected item text is long enough to fill the rest of + * the container. + */ + flex-shrink: 0; +} + +::slotted([slot="start"]) { + margin-inline-end: $form-control-label-margin; +} + +::slotted([slot="end"]) { + margin-inline-start: $form-control-label-margin; +} \ No newline at end of file diff --git a/core/src/components/select/select.tsx b/core/src/components/select/select.tsx index 6eaeb551486..b4ed9aed43c 100644 --- a/core/src/components/select/select.tsx +++ b/core/src/components/select/select.tsx @@ -33,6 +33,8 @@ import type { SelectChangeEventDetail, SelectInterface, SelectCompareFn } from ' * @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use. * * @slot label - The label text to associate with the select. Use the `labelPlacement` property to control where the label is placed relative to the select. Use this if you need to render a label with custom HTML. + * @slot start - Content to display at the leading edge of the select. + * @slot end - Content to display at the trailing edge of the select. * * @part placeholder - The text displayed in the select when there is no value. * @part text - The displayed value of the select. @@ -762,8 +764,22 @@ export class Select implements ComponentInterface { } private onClick = (ev: UIEvent) => { - this.setFocus(); - this.open(ev); + const target = ev.target as HTMLElement; + const closestSlot = target.closest('[slot="start"], [slot="end"]'); + + if (target === this.el || closestSlot === null) { + this.setFocus(); + this.open(ev); + } else { + /** + * Prevent clicks to the start/end slots from opening the select. + * We ensure the target isn't this element in case the select is slotted + * in, for example, an item. This would prevent the select from ever + * being opened since the element itself has slot="start"/"end". + */ + ev.stopPropagation(); + ev.preventDefault(); + } }; private onFocus = () => { @@ -864,8 +880,31 @@ export class Select implements ComponentInterface { const inItem = hostContext('ion-item', this.el); const shouldRenderHighlight = mode === 'md' && fill !== 'outline' && !inItem; + const hasValue = this.hasValue(); + const hasStartEndSlots = el.querySelector('[slot="start"], [slot="end"]') !== null; + renderHiddenInput(true, el, name, parseValue(value), disabled); + /** + * If the label is stacked, it should always sit above the select. + * For floating labels, the label should move above the select if + * the select has a value, is open, or has anything in either + * the start or end slot. + * + * If there is content in the start slot, the label would overlap + * it if not forced to float. This is also applied to the end slot + * because with the default or solid fills, the select is not + * vertically centered in the container, but the label is. This + * causes the slots and label to appear vertically offset from each + * other when the label isn't floating above the input. This doesn't + * apply to the outline fill, but this was not accounted for to keep + * things consistent. + * + * TODO(FW-5592): Remove hasStartEndSlots condition + */ + const labelShouldFloat = + labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || isExpanded || hasStartEndSlots)); + return (