diff --git a/BREAKING.md b/BREAKING.md index aec8b18c6a..bdc5c87e36 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -228,7 +228,6 @@ Ionic now listens on the `keydown` event instead of the `keyup` event when deter | ----------------------- | -------------- | --------- | | `--placeholder-opacity` | `0.33` | `0.6` | -

Slides

`ion-slides`, `ion-slide`, and the `IonicSwiper` plugin have been removed from Ionic. diff --git a/angular/src/directives/proxies.ts b/angular/src/directives/proxies.ts index fa9f973520..ea84ae9a55 100644 --- a/angular/src/directives/proxies.ts +++ b/angular/src/directives/proxies.ts @@ -1903,14 +1903,14 @@ export declare interface IonSelect extends Components.IonSelect { @ProxyCmp({ defineCustomElementFn: undefined, - inputs: ['cancelText', 'compareWith', 'disabled', 'interface', 'interfaceOptions', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'value'], + inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'legacy', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'value'], methods: ['open'] }) @Component({ selector: 'ion-select', changeDetection: ChangeDetectionStrategy.OnPush, template: '', - inputs: ['cancelText', 'compareWith', 'disabled', 'interface', 'interfaceOptions', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'value'] + inputs: ['cancelText', 'color', 'compareWith', 'disabled', 'fill', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'legacy', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'selectedText', 'shape', 'value'] }) export class IonSelect { protected el: HTMLElement; diff --git a/core/api.txt b/core/api.txt index 846141e229..05c9700dac 100644 --- a/core/api.txt +++ b/core/api.txt @@ -1228,16 +1228,23 @@ ion-segment-button,part,native ion-select,shadow ion-select,prop,cancelText,string,'Cancel',false,false +ion-select,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record | undefined,undefined,false,true ion-select,prop,compareWith,((currentValue: any, compareValue: any) => boolean) | null | string | undefined,undefined,false,false ion-select,prop,disabled,boolean,false,false,false +ion-select,prop,fill,"outline" | "solid" | undefined,undefined,false,false ion-select,prop,interface,"action-sheet" | "alert" | "popover",'alert',false,false ion-select,prop,interfaceOptions,any,{},false,false +ion-select,prop,justify,"end" | "space-between" | "start",'space-between',false,false +ion-select,prop,label,string | undefined,undefined,false,false +ion-select,prop,labelPlacement,"end" | "fixed" | "floating" | "stacked" | "start" | undefined,'start',false,false +ion-select,prop,legacy,boolean | undefined,undefined,false,false ion-select,prop,mode,"ios" | "md",undefined,false,false ion-select,prop,multiple,boolean,false,false,false ion-select,prop,name,string,this.inputId,false,false ion-select,prop,okText,string,'OK',false,false ion-select,prop,placeholder,string | undefined,undefined,false,false ion-select,prop,selectedText,null | string | undefined,undefined,false,false +ion-select,prop,shape,"round" | undefined,undefined,false,false ion-select,prop,value,any,undefined,false,false ion-select,method,open,open(event?: UIEvent) => Promise ion-select,event,ionBlur,void,true @@ -1245,12 +1252,21 @@ ion-select,event,ionCancel,void,true ion-select,event,ionChange,SelectChangeEventDetail,true ion-select,event,ionDismiss,void,true ion-select,event,ionFocus,void,true +ion-select,css-prop,--background +ion-select,css-prop,--border-color Color of the select border +ion-select,css-prop,--border-radius Radius of the select border +ion-select,css-prop,--border-style Style of the select border +ion-select,css-prop,--border-width Width of the select border +ion-select,css-prop,--highlight-color-focused The color of the highlight on the select when focused +ion-select,css-prop,--highlight-color-invalid The color of the highlight on the select when invalid +ion-select,css-prop,--highlight-color-valid The color of the highlight on the select when valid ion-select,css-prop,--padding-bottom ion-select,css-prop,--padding-end ion-select,css-prop,--padding-start ion-select,css-prop,--padding-top ion-select,css-prop,--placeholder-color ion-select,css-prop,--placeholder-opacity +ion-select,css-prop,--ripple-color ion-select,part,icon ion-select,part,placeholder ion-select,part,text diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 360df54fde..0ef32926a5 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -2595,6 +2595,10 @@ export namespace Components { * The text to display on the cancel button. */ "cancelText": string; + /** + * 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). This property is only available when using the modern select syntax. + */ + "color"?: Color; /** * A property name or function used to compare object values */ @@ -2603,6 +2607,10 @@ export namespace Components { * If `true`, the user cannot interact with the select. */ "disabled": boolean; + /** + * The fill for the item. If `'solid'` the item will have a background. If `'outline'` the item will be transparent with a border. Only available in `md` mode. + */ + "fill"?: 'outline' | 'solid'; /** * The interface the select should use: `action-sheet`, `popover` or `alert`. */ @@ -2611,6 +2619,22 @@ export namespace Components { * Any additional options that the `alert`, `action-sheet` or `popover` interface can take. See the [ion-alert docs](./alert), the [ion-action-sheet docs](./action-sheet) and the [ion-popover docs](./popover) for the create options for each interface. Note: `interfaceOptions` will not override `inputs` or `buttons` with the `alert` interface. */ "interfaceOptions": any; + /** + * How to pack the label and select within a line. `justify` does not apply when the label and select are on different lines when `labelPlacement` is set to `'floating'` or `'stacked'`. `'start'`: The label and select will appear on the left in LTR and on the right in RTL. `'end'`: The label and select will appear on the right in LTR and on the left in RTL. `'space-between'`: The label and select will appear on opposite ends of the line with space between the two elements. + */ + "justify": 'start' | 'end' | 'space-between'; + /** + * The visible label associated with the select. + */ + "label"?: string; + /** + * Where to place the label relative to the select. `'start'`: The label will appear to the left of the select in LTR and to the right in RTL. `'end'`: The label will appear to the right of the select in LTR and to the left in RTL. `'floating'`: The label will appear smaller and above the select when the select is focused or it has a value. Otherwise it will appear on top of the select. `'stacked'`: The label will appear smaller and above the select regardless even when the select is blurred or has no value. `'fixed'`: The label has the same behavior as `'start'` except it also has a fixed width. Long text will be truncated with ellipses ("..."). When using `'floating'` or `'stacked'` we recommend initializing the select with either a `value` or a `placeholder`. + */ + "labelPlacement"?: 'start' | 'end' | 'floating' | 'stacked' | 'fixed'; + /** + * Set the `legacy` property to `true` to forcibly use the legacy form control markup. Ionic will only opt components in to the modern form markup when they are using either the `aria-label` attribute or the `label` property. As a result, the `legacy` property should only be used as an escape hatch when you want to avoid this automatic opt-in behavior. Note that this property will be removed in an upcoming major release of Ionic, and all form components will be opted-in to using the modern form markup. + */ + "legacy"?: boolean; /** * The mode determines which platform styles to use. */ @@ -2640,6 +2664,10 @@ export namespace Components { * The text to display instead of the selected option's value. */ "selectedText"?: string | null; + /** + * The shape of the select. If "round" it will have an increased border radius. + */ + "shape"?: 'round'; /** * the value of the select. */ @@ -6589,6 +6617,10 @@ declare namespace LocalJSX { * The text to display on the cancel button. */ "cancelText"?: string; + /** + * 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). This property is only available when using the modern select syntax. + */ + "color"?: Color; /** * A property name or function used to compare object values */ @@ -6597,6 +6629,10 @@ declare namespace LocalJSX { * If `true`, the user cannot interact with the select. */ "disabled"?: boolean; + /** + * The fill for the item. If `'solid'` the item will have a background. If `'outline'` the item will be transparent with a border. Only available in `md` mode. + */ + "fill"?: 'outline' | 'solid'; /** * The interface the select should use: `action-sheet`, `popover` or `alert`. */ @@ -6605,6 +6641,22 @@ declare namespace LocalJSX { * Any additional options that the `alert`, `action-sheet` or `popover` interface can take. See the [ion-alert docs](./alert), the [ion-action-sheet docs](./action-sheet) and the [ion-popover docs](./popover) for the create options for each interface. Note: `interfaceOptions` will not override `inputs` or `buttons` with the `alert` interface. */ "interfaceOptions"?: any; + /** + * How to pack the label and select within a line. `justify` does not apply when the label and select are on different lines when `labelPlacement` is set to `'floating'` or `'stacked'`. `'start'`: The label and select will appear on the left in LTR and on the right in RTL. `'end'`: The label and select will appear on the right in LTR and on the left in RTL. `'space-between'`: The label and select will appear on opposite ends of the line with space between the two elements. + */ + "justify"?: 'start' | 'end' | 'space-between'; + /** + * The visible label associated with the select. + */ + "label"?: string; + /** + * Where to place the label relative to the select. `'start'`: The label will appear to the left of the select in LTR and to the right in RTL. `'end'`: The label will appear to the right of the select in LTR and to the left in RTL. `'floating'`: The label will appear smaller and above the select when the select is focused or it has a value. Otherwise it will appear on top of the select. `'stacked'`: The label will appear smaller and above the select regardless even when the select is blurred or has no value. `'fixed'`: The label has the same behavior as `'start'` except it also has a fixed width. Long text will be truncated with ellipses ("..."). When using `'floating'` or `'stacked'` we recommend initializing the select with either a `value` or a `placeholder`. + */ + "labelPlacement"?: 'start' | 'end' | 'floating' | 'stacked' | 'fixed'; + /** + * Set the `legacy` property to `true` to forcibly use the legacy form control markup. Ionic will only opt components in to the modern form markup when they are using either the `aria-label` attribute or the `label` property. As a result, the `legacy` property should only be used as an escape hatch when you want to avoid this automatic opt-in behavior. Note that this property will be removed in an upcoming major release of Ionic, and all form components will be opted-in to using the modern form markup. + */ + "legacy"?: boolean; /** * The mode determines which platform styles to use. */ @@ -6653,6 +6705,10 @@ declare namespace LocalJSX { * The text to display instead of the selected option's value. */ "selectedText"?: string | null; + /** + * The shape of the select. If "round" it will have an increased border radius. + */ + "shape"?: 'round'; /** * the value of the select. */ diff --git a/core/src/components/item/item.ios.scss b/core/src/components/item/item.ios.scss index fe9a399d31..9f6440450b 100644 --- a/core/src/components/item/item.ios.scss +++ b/core/src/components/item/item.ios.scss @@ -199,8 +199,8 @@ --min-height: 68px; } -:host(.item-label-stacked) ::slotted(ion-select), -:host(.item-label-floating) ::slotted(ion-select) { +:host(.item-label-stacked) ::slotted(ion-select.legacy-select), +:host(.item-label-floating) ::slotted(ion-select.legacy-select) { --padding-top: 8px; --padding-bottom: 8px; --padding-start: 0px; @@ -210,7 +210,7 @@ // iOS Fixed Labels // -------------------------------------------------- -:host(.item-label-fixed) ::slotted(ion-select), +:host(.item-label-fixed) ::slotted(ion-select.legacy-select), :host(.item-label-fixed) ::slotted(ion-datetime) { --padding-start: 0; } diff --git a/core/src/components/item/item.md.scss b/core/src/components/item/item.md.scss index 66570ae0b4..d78ce0557f 100644 --- a/core/src/components/item/item.md.scss +++ b/core/src/components/item/item.md.scss @@ -315,7 +315,7 @@ // Material Design Fixed Labels // -------------------------------------------------- -:host(.item-label-fixed) ::slotted(ion-select), +:host(.item-label-fixed) ::slotted(ion-select.legacy-select), :host(.item-label-fixed) ::slotted(ion-datetime) { --padding-start: 8px; } @@ -361,8 +361,8 @@ --min-height: 55px; } -:host(.item-label-stacked) ::slotted(ion-select), -:host(.item-label-floating) ::slotted(ion-select) { +:host(.item-label-stacked) ::slotted(ion-select.legacy-select), +:host(.item-label-floating) ::slotted(ion-select.legacy-select) { --padding-top: 8px; --padding-bottom: 8px; --padding-start: 0; diff --git a/core/src/components/item/item.scss b/core/src/components/item/item.scss index 6ccf5054e3..402b8eb6ed 100644 --- a/core/src/components/item/item.scss +++ b/core/src/components/item/item.scss @@ -490,14 +490,14 @@ button, a { // Item Select // -------------------------------------------------- -:host(:not(.item-label)) ::slotted(ion-select) { +:host(:not(.item-label)) ::slotted(ion-select.legacy-select) { --padding-start: 0; max-width: none; } -:host(.item-label-stacked) ::slotted(ion-select), -:host(.item-label-floating) ::slotted(ion-select) { +:host(.item-label-stacked) ::slotted(ion-select.legacy-select), +:host(.item-label-floating) ::slotted(ion-select.legacy-select) { --padding-top: 8px; --padding-bottom: 8px; --padding-start: 0; @@ -533,7 +533,7 @@ button, a { :host(.item-multiple-inputs) ::slotted(ion-checkbox), :host(.item-multiple-inputs) ::slotted(ion-datetime), :host(.item-multiple-inputs) ::slotted(ion-radio), -:host(.item-multiple-inputs) ::slotted(ion-select) { +:host(.item-multiple-inputs) ::slotted(ion-select.legacy-select) { position: relative; } diff --git a/core/src/components/select/select.ios.scss b/core/src/components/select/select.ios.scss index 25a6645225..fa54e598cc 100644 --- a/core/src/components/select/select.ios.scss +++ b/core/src/components/select/select.ios.scss @@ -4,13 +4,27 @@ // iOS Select // -------------------------------------------------- -:host { +:host(.legacy-select) { --padding-top: #{$select-ios-padding-top}; --padding-end: #{$select-ios-padding-end}; --padding-bottom: #{$select-ios-padding-bottom}; --padding-start: #{$select-ios-padding-start}; } -.select-icon { - opacity: .33; +:host(:not(.legacy-select)) { + min-height: 44px; +} + +/** + * Since the label sits on top of the element, + * the component needs to be taller otherwise the + * label will appear too close to the select text. + */ +:host(.select-label-placement-floating), +:host(.select-label-placement-stacked) { + min-height: 56px; +} + +.select-icon { + color: #{$text-color-step-700}; } diff --git a/core/src/components/select/select.md.outline.scss b/core/src/components/select/select.md.outline.scss new file mode 100644 index 0000000000..4664152ca0 --- /dev/null +++ b/core/src/components/select/select.md.outline.scss @@ -0,0 +1,258 @@ +@import "./select.vars"; + +// Select Fill: Outline +// ---------------------------------------------------------------- + +:host(.select-fill-outline) { + --border-color: #{$background-color-step-300}; + --border-radius: 4px; + --padding-start: 16px; + --padding-end: 16px; +} + +:host(.select-fill-outline.select-shape-round) { + --border-radius: 28px; + --padding-start: 32px; + --padding-end: 32px; +} + +/** + * If the select has a validity state, the + * border should reflect that as a color. + */ +:host(.select-fill-outline.ion-touched.ion-valid), +:host(.select-fill-outline.ion-touched.ion-invalid) { + --border-color: var(--highlight-color); +} + +/** + * Border should be + * slightly darker on hover. + */ +@media (any-hover: hover) { + :host(.select-fill-outline:hover) { + --border-color: #{$background-color-step-750}; + } +} + +/** + * The border should get thicker + * and take on component color when + * the select is focused. + */ +:host(.select-fill-outline.select-expanded), +:host(.select-fill-outline.ion-focused) { + --border-width: 2px; + --border-color: var(--highlight-color); +} + +/** + * The bottom content should never have + * a border with the outline style. + */ +:host(.select-fill-outline) .select-bottom { + border-top: none; +} + +/** + * Outline selects do not have a bottom border. + * Instead, they have a border that wraps the + * select + label. + */ +:host(.select-fill-outline) .select-wrapper { + border-bottom: none; +} + +:host(.select-ltr.select-fill-outline.select-label-placement-stacked) .label-text-wrapper, +:host(.select-ltr.select-fill-outline.select-label-placement-floating) .label-text-wrapper { + // stylelint-disable-next-line property-disallowed-list + transform-origin: left top; +} + +:host(.select-rtl.select-fill-outline.select-label-placement-stacked) .label-text-wrapper, +:host(.select-rtl.select-fill-outline.select-label-placement-floating) .label-text-wrapper { + // stylelint-disable-next-line property-disallowed-list + transform-origin: right top; +} + +:host(.select-fill-outline.select-label-placement-stacked) .label-text-wrapper, +:host(.select-fill-outline.select-label-placement-floating) .label-text-wrapper { + position: absolute; + + /** + * Label text should not extend + * beyond the bounds of the select. + */ + max-width: calc(100% - var(--padding-start) - var(--padding-end)); +} + +/** + * The label should appear on top of an outline + * container that overlaps it so it is always clickable. + */ +:host(.select-fill-outline) .label-text-wrapper, +:host(.select-fill-outline) .label-text-wrapper { + position: relative; + + z-index: 1; +} + +/** + * 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 { + @include transform(translateY(-32%), scale(#{$select-floating-label-scale})); + @include margin(0); + + /** + * Label text should not extend + * beyond the bounds of the select. + */ + max-width: calc((100% - var(--padding-start) - var(--padding-end) - #{$select-md-floating-label-padding * 2}) / #{$select-floating-label-scale}); +} + +/** + * This ensures that the select does not + * overlap the floating label while still + * remaining visually centered. + */ +:host(.select-fill-outline.select-label-placement-stacked) select, +:host(.select-fill-outline.select-label-placement-floating) select { + @include margin(6px, 0, 6px, 0); +} + +// Select Fill: Outline Outline Container +// ---------------------------------------------------------------- + +:host(.select-fill-outline) .select-outline-container { + @include position(0, 0, 0, 0); + + display: flex; + + position: absolute; + + width: 100%; + height: 100%; +} + +:host(.select-fill-outline) .select-outline-start, +:host(.select-fill-outline) .select-outline-end { + pointer-events: none; +} + +/** + * By default, each piece of the container should have + * a top and bottom border. This gives the appearance + * of a unified container with a border. + */ +:host(.select-fill-outline) .select-outline-start, +:host(.select-fill-outline) .select-outline-notch, +:host(.select-fill-outline) .select-outline-end { + border-top: var(--border-width) var(--border-style) var(--border-color); + border-bottom: var(--border-width) var(--border-style) var(--border-color); + + /** + * `border-box` is applied in the global + * Ionic stylesheet, but since this is in + * the Shadow DOM, these elements do not + * receive the global style. The outline + * pieces for `ion-input` do because that + * component is in the Light DOM. + */ + box-sizing: border-box; +} + +/** + * Ensures long labels do not cause the notch to flow + * out of bounds. + */ +:host(.select-fill-outline) .select-outline-notch { + max-width: calc(100% - var(--padding-start) - var(--padding-end)); +} + +/** + * This element ensures that the notch used + * the size of the scaled text so that the + * border cut out is the correct width. + * The text in this element should not + * be interactive. + */ +:host(.select-fill-outline) .notch-spacer { + /** + * We need $select-md-floating-label-padding of padding on the right. + * However, we also subtracted $select-md-floating-label-padding from + * the width of .select-outline-start + * to create space, so we need to take + * that into consideration here. + */ + @include padding(null, #{$select-md-floating-label-padding * 2}, null, null); + + font-size: calc(1em * #{$select-floating-label-scale}); + + opacity: 0; + pointer-events: none; +} + +:host(.select-ltr.select-fill-outline) .select-outline-start { + // stylelint-disable-next-line property-disallowed-list + border-left: var(--border-width) var(--border-style) var(--border-color); + // stylelint-disable-next-line property-disallowed-list + border-radius: var(--border-radius) 0px 0px var(--border-radius); +} + +:host(.select-rtl.select-fill-outline) .select-outline-start { + // stylelint-disable-next-line property-disallowed-list + border-right: var(--border-width) var(--border-style) var(--border-color); + // stylelint-disable-next-line property-disallowed-list + border-radius: 0px var(--border-radius) var(--border-radius) 0px; +} + +:host(.select-fill-outline) .select-outline-start { + /** + * There should be spacing between the translated text + * and .select-outline-start. However, we can't add this + * spacing onto the notch because it would cause the + * label to look like it is not aligned with the + * text select. Instead, we subtract a few pixels from + * this element. + */ + width: calc(var(--padding-start) - #{$select-md-floating-label-padding}); +} + +:host(.select-ltr.select-fill-outline) .select-outline-end { + // stylelint-disable-next-line property-disallowed-list + border-right: var(--border-width) var(--border-style) var(--border-color); + // stylelint-disable-next-line property-disallowed-list + border-radius: 0px var(--border-radius) var(--border-radius) 0px; +} + +:host(.select-rtl.select-fill-outline) .select-outline-end { + // stylelint-disable-next-line property-disallowed-list + border-left: var(--border-width) var(--border-style) var(--border-color); + // stylelint-disable-next-line property-disallowed-list + border-radius: var(--border-radius) 0px 0px var(--border-radius); +} + +:host(.select-fill-outline) .select-outline-end { + /** + * The ending outline fragment + * should take up the remaining free space. + */ + flex-grow: 1; +} + +/** + * When the select either has focus or a value, + * there should be a "cut out" at the top for + * 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 { + border-top: none; +} diff --git a/core/src/components/select/select.md.scss b/core/src/components/select/select.md.scss index 36d8dccbf5..b7aba1588a 100644 --- a/core/src/components/select/select.md.scss +++ b/core/src/components/select/select.md.scss @@ -1,21 +1,110 @@ @import "./select"; @import "./select.md.vars"; +@import "./select.md.solid.scss"; +@import "./select.md.outline.scss"; // Material Design Select // -------------------------------------------------- :host { + --border-width: 1px; + --border-color: #{$item-md-border-color}; +} + +:host(.legacy-select) { --padding-top: #{$select-md-padding-top}; --padding-end: #{$select-md-padding-end}; --padding-bottom: #{$select-md-padding-bottom}; --padding-start: #{$select-md-padding-start}; } +:host(:not(.legacy-select)) { + min-height: 56px; +} + .select-icon { transition: transform .15s cubic-bezier(.4, 0, .2, 1); - opacity: .55; + color: #{$text-color-step-500}; +} +// Select Label +// ---------------------------------------------------------------- + +/** + * When the select is focused the label should + * take on the highlight color. This should + * only apply to floating or stacked labels. + */ +:host(.select-label-placement-floating.select-expanded) .label-text-wrapper, +:host(.select-label-placement-floating.ion-focused) .label-text-wrapper, +:host(.select-label-placement-stacked.select-expanded) .label-text-wrapper, +:host(.select-label-placement-stacked.ion-focused) .label-text-wrapper { + color: var(--highlight-color); +} + +:host(.select-label-placement-floating.ion-touched.ion-valid) .label-text-wrapper, +:host(.select-label-placement-floating.ion-touched.ion-invalid) .label-text-wrapper, +:host(.select-label-placement-stacked.ion-touched.ion-valid) .label-text-wrapper, +:host(.select-label-placement-stacked.ion-touched.ion-invalid) .label-text-wrapper { + color: var(--highlight-color); +} + +// Select Highlight +// ---------------------------------------------------------------- + +.select-highlight { + @include position(null, null, -1px, 0); + + position: absolute; + + width: 100%; + height: 2px; + + transform: scale(0); + + transition: transform 200ms; + + background: var(--highlight-color); +} + +:host(.select-expanded) .select-highlight, +:host(.ion-focused) .select-highlight { + transform: scale(1); +} + +/** + * Adjust the highlight up by 1px + * so it is not cut off by the + * the item's line (if one is present). + */ +:host(.in-item) .select-highlight { + @include position(null, null, 0, 0); +} + +// Select Icon +// ---------------------------------------------------------------- + +/** + * This rotates the chevron icon + * when the select is activated. + * This should only happen on MD. + */ +:host(.select-expanded:not(.legacy-select)) .select-icon { + @include transform(rotate(180deg)); +} + +/** + * When the select is focused the icon should + * take on the highlight color. + * The icon should also take on the highlight + * color if there is a validation state. + */ +:host(.select-expanded) .select-wrapper .select-icon, +:host(.ion-touched.ion-valid) .select-wrapper .select-icon, +:host(.ion-touched.ion-invalid) .select-wrapper .select-icon, +:host(.ion-focused) .select-wrapper .select-icon { + color: var(--highlight-color); } /** @@ -43,9 +132,9 @@ @include transform(rotate(180deg), translate3d(0, -9px, 0)); } -:host-context(ion-item.ion-focused) .select-icon, -:host-context(.item-has-focus) .select-icon { - color: var(--highlight-color-focused); +// Select Shape Rounded +// ---------------------------------------------------------------- - opacity: 1; +:host(.select-shape-round) { + --border-radius: 16px; } diff --git a/core/src/components/select/select.md.solid.scss b/core/src/components/select/select.md.solid.scss new file mode 100644 index 0000000000..9af5003b60 --- /dev/null +++ b/core/src/components/select/select.md.solid.scss @@ -0,0 +1,78 @@ +@import "./select.vars"; + +// Select Fill: Solid +// ---------------------------------------------------------------- + +:host(.select-fill-solid) { + --background: #{$background-color-step-50}; + --border-color: #{$background-color-step-500}; + --border-radius: 4px; + --padding-start: 16px; + --padding-end: 16px; +} + +/** + * The solid fill style has a border + * at the bottom of the select wrapper. + * As a result, the border on the "bottom + * content" is not needed. + */ +:host(.select-fill-solid) .select-wrapper { + border-bottom: var(--border-width) var(--border-style) var(--border-color); +} + +/** + * If the select has a validity state, the + * border should reflect that as a color. + */ +:host(.select-fill-solid.ion-touched.ion-valid) .select-wrapper, +:host(.select-fill-solid.ion-touched.ion-invalid) .select-wrapper { + --border-color: var(--highlight-color); +} + +:host(.select-fill-solid) .select-bottom { + border-top: none; +} + +/** + * Background and border should be + * slightly darker on hover. + */ +@media (any-hover: hover) { + :host(.select-fill-solid:hover) { + --background: #{$background-color-step-100}; + --border-color: #{$background-color-step-750}; + } +} + +/** + * Background and border should be + * much darker on focus. + */ +:host(.select-fill-solid.select-expanded), +:host(.select-fill-solid.ion-focused) { + --background: #{$background-color-step-150}; + --border-color: #{$background-color-step-750}; +} + +:host(.select-fill-solid) .select-wrapper { + /** + * Only the top left and top right borders should. + * have a radius when using a solid fill. + */ + @include border-radius(var(--border-radius), var(--border-radius), 0px, 0px); +} + +// 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 { + /** + * Label text should not extend + * beyond the bounds of the select. + */ + max-width: calc(100% / #{$select-floating-label-scale}); +} diff --git a/core/src/components/select/select.md.vars.scss b/core/src/components/select/select.md.vars.scss index ef6f37020b..ea20b33d5b 100644 --- a/core/src/components/select/select.md.vars.scss +++ b/core/src/components/select/select.md.vars.scss @@ -24,3 +24,6 @@ $select-md-placeholder-color: $select-md-icon-color !default; /// @prop - Text Color of the selected item $select-md-text-color: $text-color !default; + +/// @prop - The amount of whitespace to display on either side of the floating label +$select-md-floating-label-padding: 4px !default; diff --git a/core/src/components/select/select.scss b/core/src/components/select/select.scss index 6d2d7a9019..6423398941 100644 --- a/core/src/components/select/select.scss +++ b/core/src/components/select/select.scss @@ -5,6 +5,7 @@ :host { /** + * @prop --background: Background of the select * @prop --padding-top: Top padding of the select * @prop --padding-end: Right padding if direction is left-to-right, and left padding if direction is right-to-left of the select * @prop --padding-bottom: Bottom padding of the select @@ -12,29 +13,75 @@ * * @prop --placeholder-color: Color of the select placeholder text * @prop --placeholder-opacity: Opacity of the select placeholder text + * + * @prop --highlight-color-focused The color of the highlight on the select when focused + * @prop --highlight-color-invalid The color of the highlight on the select when invalid + * @prop --highlight-color-valid The color of the highlight on the select when valid + * + * @prop --border-color Color of the select border + * @prop --border-radius Radius of the select border + * @prop --border-style Style of the select border + * @prop --border-width Width of the select border + * + * @prop --ripple-color: The color of the ripple effect on MD mode. */ + --padding-top: 0px; + --padding-end: 0px; + --padding-bottom: 0px; + --padding-start: 0px; --placeholder-color: currentColor; --placeholder-opacity: #{$placeholder-opacity}; + --background: transparent; + --border-style: solid; + --highlight-color-focused: #{ion-color(primary, base)}; + --highlight-color-valid: #{ion-color(success, base)}; + --highlight-color-invalid: #{ion-color(danger, base)}; - @include padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start)); + /** + * This is a private API that is used to switch + * out the highlight color based on the state + * of the component without having to write + * different selectors for different fill variants. + */ + --highlight-color: var(--highlight-color-focused); - display: flex; + display: block; position: relative; - align-items: center; - font-family: $font-family-base; - overflow: hidden; + cursor: pointer; + z-index: $z-index-item-input; } -:host(.in-item) { +:host(.ion-color) { + --highlight-color-focused: #{current-color(base)}; +} + +:host(.legacy-select) { + @include padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start)); + + display: flex; + + align-items: center; + + overflow: hidden; +} + +// Select Used in ion-item +// -------------------------------------------------- +:host(.in-item.legacy-select) { position: static; max-width: 45%; } +:host(.in-item:not(.legacy-select)) { + width: 100%; + height: 100%; +} + :host(.select-disabled) { opacity: .4; pointer-events: none; @@ -50,7 +97,7 @@ opacity: var(--placeholder-opacity); } -label { +:host(.legacy-select) label { @include input-cover(); display: flex; @@ -64,6 +111,9 @@ button { @include visually-hidden(); } +// Select Icon +// -------------------------------------------------- + .select-icon { @include margin(0, 0, 0, 4px); @@ -72,6 +122,47 @@ button { width: 13px; } +/** + * Ensure that the select icon has + * the correct color contrast when + * used inside of an item. + */ +:host(.in-item-color) .select-icon { + color: inherit; +} + +/** + * The select icon should be centered with + * the entire container not just the control + * with floating/stacked labels. + */ +:host(.select-label-placement-stacked) .select-icon, +:host(.select-label-placement-floating) .select-icon { + position: absolute; + + height: 100%; +} + +/** + * This positions the icon at the correct + * edge of the component with LTR and RTL + * text directions. The position mixin cannot be + * used here because the icon is in the Shadow DOM. + */ +:host(.select-ltr.select-label-placement-stacked) .select-icon, +:host(.select-ltr.select-label-placement-floating) .select-icon { + // stylelint-disable-next-line property-disallowed-list + right: var(--padding-end, 0); +} + +:host(.select-rtl.select-label-placement-stacked) .select-icon, +:host(.select-rtl.select-label-placement-floating) .select-icon { + // stylelint-disable-next-line property-disallowed-list + left: var(--padding-start, 0); +} + +// Select Text +// -------------------------------------------------- .select-text { flex: 1; @@ -85,3 +176,302 @@ button { overflow: hidden; } + +// Select Wrapper +// -------------------------------------------------- + +.select-wrapper { + @include padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start)); + + display: flex; + + position: relative; + + flex-grow: 1; + + align-items: center; + + height: 100%; + + /** + * This allows developers to set the height + * on the host of the element but still have + * the label take up the full height + * of the parent. + */ + min-height: inherit; + + transition: background-color 15ms linear; + + background: var(--background); + + cursor: inherit; + + box-sizing: border-box; +} + +// Select Highlight +// ---------------------------------------------------------------- + +:host(.ion-touched.ion-invalid) { + --highlight-color: var(--highlight-color-invalid); +} + +:host(.ion-touched.ion-valid) { + --highlight-color: var(--highlight-color-valid); +} + +// Select Label +// ---------------------------------------------------------------- + +.label-text-wrapper { + /** + * This causes the label to take up + * the entire height of its container + * while still keeping the text centered. + */ + display: flex; + + align-items: center; + + /** + * Label text should not extend + * beyond the bounds of the select. + * However, we do not set the max + * width to 100% because then + * only the label would show and users + * would not be able to see what they are typing. + */ + max-width: 200px; + + transition: color 150ms cubic-bezier(.4, 0, .2, 1), transform 150ms cubic-bezier(.4, 0, .2, 1); + + /** + * This ensures that double tapping this text + * clicks the