feat(select): update popover interface to match MD spec on desktop, allow multiple values in popover interface (#23474)

resolves #23657
resolves #15500
resolves #12310
This commit is contained in:
Brandy Carney
2021-07-20 11:23:00 -04:00
committed by GitHub
parent be219a2814
commit 2c07a1566b
28 changed files with 889 additions and 92 deletions

View File

@ -208,6 +208,7 @@ export namespace Components {
"translucent": boolean; "translucent": boolean;
} }
interface IonApp { interface IonApp {
"setFocus": (elements: HTMLElement[]) => Promise<void>;
} }
interface IonAvatar { interface IonAvatar {
} }
@ -2358,19 +2359,23 @@ export namespace Components {
} }
interface IonSelectPopover { interface IonSelectPopover {
/** /**
* Header text for the popover * The header text of the popover
*/ */
"header"?: string; "header"?: string;
/** /**
* Text for popover body * The text content of the popover body
*/ */
"message"?: string; "message"?: string;
/** /**
* Array of options for the popover * If true, the select accepts multiple values
*/
"multiple"?: boolean;
/**
* An array of options for the popover
*/ */
"options": SelectPopoverOption[]; "options": SelectPopoverOption[];
/** /**
* Subheader text for the popover * The subheader text of the popover
*/ */
"subHeader"?: string; "subHeader"?: string;
} }
@ -5946,19 +5951,23 @@ declare namespace LocalJSX {
} }
interface IonSelectPopover { interface IonSelectPopover {
/** /**
* Header text for the popover * The header text of the popover
*/ */
"header"?: string; "header"?: string;
/** /**
* Text for popover body * The text content of the popover body
*/ */
"message"?: string; "message"?: string;
/** /**
* Array of options for the popover * If true, the select accepts multiple values
*/
"multiple"?: boolean;
/**
* An array of options for the popover
*/ */
"options"?: SelectPopoverOption[]; "options"?: SelectPopoverOption[];
/** /**
* Subheader text for the popover * The subheader text of the popover
*/ */
"subHeader"?: string; "subHeader"?: string;
} }

View File

@ -1,4 +1,4 @@
import { Build, Component, ComponentInterface, Element, Host, h } from '@stencil/core'; import { Build, Component, ComponentInterface, Element, Host, Method, h } from '@stencil/core';
import { config } from '../../global/config'; import { config } from '../../global/config';
import { getIonMode } from '../../global/ionic-global'; import { getIonMode } from '../../global/ionic-global';
@ -9,6 +9,8 @@ import { isPlatform } from '../../utils/platform';
styleUrl: 'app.scss', styleUrl: 'app.scss',
}) })
export class App implements ComponentInterface { export class App implements ComponentInterface {
private focusVisible?: any;
@Element() el!: HTMLElement; @Element() el!: HTMLElement;
componentDidLoad() { componentDidLoad() {
@ -33,11 +35,28 @@ export class App implements ComponentInterface {
if (typeof (window as any) !== 'undefined') { if (typeof (window as any) !== 'undefined') {
import('../../utils/keyboard/keyboard').then(module => module.startKeyboardAssist(window)); import('../../utils/keyboard/keyboard').then(module => module.startKeyboardAssist(window));
} }
import('../../utils/focus-visible').then(module => module.startFocusVisible()); import('../../utils/focus-visible').then(module => this.focusVisible = module.startFocusVisible());
}); });
} }
} }
/**
* @internal
* Used to set focus on an element that uses `ion-focusable`.
* Do not use this if focusing the element as a result of a keyboard
* event as the focus utility should handle this for us. This method
* should be used when we want to programmatically focus an element as
* a result of another user action. (Ex: We focus the first element
* inside of a popover when the user presents it, but the popover is not always
* presented as a result of keyboard action.)
*/
@Method()
async setFocus(elements: HTMLElement[]) {
if (this.focusVisible) {
this.focusVisible.setFocus(elements);
}
}
render() { render() {
const mode = getIonMode(this); const mode = getIonMode(this);
return ( return (

View File

@ -303,6 +303,19 @@ export default defineComponent({
| `--transition` | Transition of the checkbox icon | | `--transition` | Transition of the checkbox icon |
## Dependencies
### Used by
- ion-select-popover
### Graph
```mermaid
graph TD;
ion-select-popover --> ion-checkbox
style ion-checkbox fill:#f9f,stroke:#333,stroke-width:4px
```
---------------------------------------------- ----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)* *Built with [StencilJS](https://stenciljs.com/)*

View File

@ -760,7 +760,7 @@ export class Datetime implements ComponentInterface {
} }
connectedCallback() { connectedCallback() {
this.clearFocusVisible = startFocusVisible(this.el); this.clearFocusVisible = startFocusVisible(this.el).destroy;
} }
disconnectedCallback() { disconnectedCallback() {

View File

@ -24,6 +24,7 @@
--highlight-color-valid: #{$item-ios-input-highlight-color-valid}; --highlight-color-valid: #{$item-ios-input-highlight-color-valid};
--highlight-color-invalid: #{$item-ios-input-highlight-color-invalid}; --highlight-color-invalid: #{$item-ios-input-highlight-color-invalid};
--bottom-padding-start: 0px; --bottom-padding-start: 0px;
font-size: $item-ios-font-size; font-size: $item-ios-font-size;
} }

View File

@ -99,6 +99,7 @@
transition: none; transition: none;
} }
:host(.item-fill-outline.ion-focused) .item-native,
:host(.item-fill-outline.item-has-focus) .item-native { :host(.item-fill-outline.item-has-focus) .item-native {
border-color: transparent; border-color: transparent;
} }
@ -308,6 +309,8 @@
--padding-start: 0; --padding-start: 0;
} }
:host(.ion-focused:not(.ion-color)) ::slotted(.label-stacked),
:host(.ion-focused:not(.ion-color)) ::slotted(.label-floating),
:host(.item-has-focus:not(.ion-color)) ::slotted(.label-stacked), :host(.item-has-focus:not(.ion-color)) ::slotted(.label-stacked),
:host(.item-has-focus:not(.ion-color)) ::slotted(.label-floating) { :host(.item-has-focus:not(.ion-color)) ::slotted(.label-floating) {
color: $label-md-text-color-focused; color: $label-md-text-color-focused;
@ -347,13 +350,10 @@
--border-color: #{$item-md-input-fill-border-color}; --border-color: #{$item-md-input-fill-border-color};
} }
:host(.item-fill-solid) .item-native:hover { :host(.item-fill-solid.ion-focused) .item-native,
--background: var(--background-hover);
--border-color: #{$item-md-input-fill-border-color-hover};
}
:host(.item-fill-solid.item-has-focus) .item-native { :host(.item-fill-solid.item-has-focus) .item-native {
--background: var(--background-focused); --background: var(--background-focused);
border-bottom-color: var(--highlight-color-focused); border-bottom-color: var(--highlight-color-focused);
} }
@ -361,10 +361,20 @@
@include border-radius(16px, 16px, 0, 0); @include border-radius(16px, 16px, 0, 0);
} }
@media (any-hover: hover) {
:host(.item-fill-solid:hover) .item-native {
--background: var(--background-hover);
--border-color: #{$item-md-input-fill-border-color-hover};
}
}
// Material Design Item: Fill Outline // Material Design Item: Fill Outline
// -------------------------------------------------- // --------------------------------------------------
:host(.item-fill-outline) { :host(.item-fill-outline) {
--ripple-color: transparent;
--background-focused: transparent;
--background-hover: transparent;
--border-color: #{$item-md-input-fill-border-color}; --border-color: #{$item-md-input-fill-border-color};
--border-width: #{$item-md-border-bottom-width}; --border-width: #{$item-md-border-bottom-width};
@ -379,10 +389,6 @@
@include border-radius(4px); @include border-radius(4px);
} }
:host(.item-fill-outline) .item-native:hover {
--border-color: #{$item-md-input-fill-border-color-hover};
}
:host(.item-fill-outline.item-shape-round) .item-native { :host(.item-fill-outline.item-shape-round) .item-native {
--inner-padding-start: 16px; --inner-padding-start: 16px;
@ -393,14 +399,22 @@
@include padding-horizontal(32px, null); @include padding-horizontal(32px, null);
} }
:host(.item-fill-outline.item-label-floating.ion-focused) .item-native ::slotted(ion-input:not(:first-child)),
:host(.item-fill-outline.item-label-floating.ion-focused) .item-native ::slotted(ion-textarea:not(:first-child)),
:host(.item-fill-outline.item-label-floating.item-has-focus) .item-native ::slotted(ion-input:not(:first-child)), :host(.item-fill-outline.item-label-floating.item-has-focus) .item-native ::slotted(ion-input:not(:first-child)),
:host(.item-fill-outline.item-label-floating.item-has-value) .item-native ::slotted(ion-input:not(:first-child)),
:host(.item-fill-outline.item-label-floating.item-has-focus) .item-native ::slotted(ion-textarea:not(:first-child)), :host(.item-fill-outline.item-label-floating.item-has-focus) .item-native ::slotted(ion-textarea:not(:first-child)),
:host(.item-fill-outline.item-label-floating.item-has-value) .item-native ::slotted(ion-input:not(:first-child)),
:host(.item-fill-outline.item-label-floating.item-has-value) .item-native ::slotted(ion-textarea:not(:first-child)) { :host(.item-fill-outline.item-label-floating.item-has-value) .item-native ::slotted(ion-textarea:not(:first-child)) {
transform: translateY(-25%); transform: translateY(-25%);
} }
@media (any-hover: hover) {
:host(.item-fill-outline:hover) .item-native {
--border-color: #{$item-md-input-fill-border-color-hover};
}
}
// Material Design Item: Invalid // Material Design Item: Invalid
// -------------------------------------------------- // --------------------------------------------------

View File

@ -154,7 +154,7 @@
// -------------------------------------------------- // --------------------------------------------------
@media (any-hover: hover) { @media (any-hover: hover) {
:host(.ion-activatable:hover) .item-native { :host(.ion-activatable:not(.ion-focused):hover) .item-native {
color: var(--color-hover); color: var(--color-hover);
&::after { &::after {
@ -164,7 +164,7 @@
} }
} }
:host(.ion-color.ion-activatable:hover) .item-native { :host(.ion-color.ion-activatable:not(.ion-focused):hover) .item-native {
color: #{current-color(contrast)}; color: #{current-color(contrast)};
&::after { &::after {
@ -173,6 +173,7 @@
} }
} }
// Item: Disabled // Item: Disabled
// -------------------------------------------------- // --------------------------------------------------
@ -308,7 +309,11 @@ button, a {
z-index: 1; z-index: 1;
} }
// Setting pointer-events to none allows the label
// to be clicked to open the select interface
::slotted(ion-label) { ::slotted(ion-label) {
pointer-events: none;
flex: 1; flex: 1;
} }
@ -370,6 +375,8 @@ button, a {
pointer-events: none; pointer-events: none;
} }
:host(.ion-focused) .item-highlight,
:host(.ion-focused) .item-inner-highlight,
:host(.item-has-focus) .item-highlight, :host(.item-has-focus) .item-highlight,
:host(.item-has-focus) .item-inner-highlight { :host(.item-has-focus) .item-inner-highlight {
transform: scaleX(1); transform: scaleX(1);
@ -378,22 +385,27 @@ button, a {
border-color: var(--highlight-background); border-color: var(--highlight-background);
} }
:host(.ion-focused) .item-highlight,
:host(.item-has-focus) .item-highlight { :host(.item-has-focus) .item-highlight {
border-width: var(--full-highlight-height); border-width: var(--full-highlight-height);
opacity: var(--show-full-highlight); opacity: var(--show-full-highlight);
} }
:host(.ion-focused) .item-inner-highlight,
:host(.item-has-focus) .item-inner-highlight { :host(.item-has-focus) .item-inner-highlight {
border-bottom-width: var(--inset-highlight-height); border-bottom-width: var(--inset-highlight-height);
opacity: var(--show-inset-highlight); opacity: var(--show-inset-highlight);
} }
:host(.ion-focused.item-fill-solid) .item-highlight,
:host(.item-has-focus.item-fill-solid) .item-highlight { :host(.item-has-focus.item-fill-solid) .item-highlight {
border-width: calc(var(--full-highlight-height) - 1px); border-width: calc(var(--full-highlight-height) - 1px);
} }
:host(.ion-focused) .item-inner-highlight,
:host(.ion-focused:not(.item-fill-outline)) .item-highlight,
:host(.item-has-focus) .item-inner-highlight, :host(.item-has-focus) .item-inner-highlight,
:host(.item-has-focus:not(.item-fill-outline)) .item-highlight { :host(.item-has-focus:not(.item-fill-outline)) .item-highlight {
border-top: none; border-top: none;
@ -405,6 +417,7 @@ button, a {
// Item Input Focused // Item Input Focused
// -------------------------------------------------- // --------------------------------------------------
:host(.item-interactive.ion-focused),
:host(.item-interactive.item-has-focus), :host(.item-interactive.item-has-focus),
:host(.item-interactive.ion-touched.ion-invalid) { :host(.item-interactive.ion-touched.ion-invalid) {
// If the item has a full border and highlight is enabled, show the full item highlight // If the item has a full border and highlight is enabled, show the full item highlight
@ -417,6 +430,7 @@ button, a {
// Item Input Focus // Item Input Focus
// -------------------------------------------------- // --------------------------------------------------
:host(.item-interactive.ion-focused),
:host(.item-interactive.item-has-focus) { :host(.item-interactive.item-has-focus) {
--highlight-background: var(--highlight-color-focused); --highlight-background: var(--highlight-color-focused);
} }

View File

@ -47,6 +47,7 @@
transform 150ms $label-md-transition-timing-function; transform 150ms $label-md-transition-timing-function;
} }
:host-context(.ion-focused).label-floating,
:host-context(.item-has-focus).label-floating, :host-context(.item-has-focus).label-floating,
:host-context(.item-has-placeholder:not(.item-input)).label-floating, :host-context(.item-has-placeholder:not(.item-input)).label-floating,
:host-context(.item-has-value).label-floating { :host-context(.item-has-value).label-floating {
@ -57,6 +58,7 @@
* When translating the label inside of an ion-item with `fill="outline"`, * When translating the label inside of an ion-item with `fill="outline"`,
* add pseudo-elements to imitate fieldset-like padding without shifting the label * add pseudo-elements to imitate fieldset-like padding without shifting the label
*/ */
:host-context(.item-fill-outline.ion-focused).label-floating,
:host-context(.item-fill-outline.item-has-focus).label-floating, :host-context(.item-fill-outline.item-has-focus).label-floating,
:host-context(.item-fill-outline.item-has-placeholder:not(.item-input)).label-floating, :host-context(.item-fill-outline.item-has-placeholder:not(.item-input)).label-floating,
:host-context(.item-fill-outline.item-has-value).label-floating { :host-context(.item-fill-outline.item-has-value).label-floating {
@ -96,28 +98,38 @@
} }
} }
:host-context(.item-fill-outline.ion-focused.item-has-start-slot).label-floating,
:host-context(.item-fill-outline.item-has-focus.item-has-start-slot).label-floating, :host-context(.item-fill-outline.item-has-focus.item-has-start-slot).label-floating,
:host-context(.item-fill-outline.item-has-placeholder:not(.item-input).item-has-start-slot).label-floating, :host-context(.item-fill-outline.item-has-placeholder:not(.item-input).item-has-start-slot).label-floating,
:host-context(.item-fill-outline.item-has-value.item-has-start-slot).label-floating { :host-context(.item-fill-outline.item-has-value.item-has-start-slot).label-floating {
@include transform(translateX(#{$item-md-fill-outline-label-translate-x}), translateY(-6px), scale(.75)); @include transform(translateX(#{$item-md-fill-outline-label-translate-x}), translateY(-6px), scale(.75));
} }
:host-context(.item-fill-outline.ion-focused.item-has-start-slot).label-floating.label-rtl,
:host-context(.item-fill-outline.item-has-focus.item-has-start-slot).label-floating.label-rtl, :host-context(.item-fill-outline.item-has-focus.item-has-start-slot).label-floating.label-rtl,
:host-context(.item-fill-outline.item-has-placeholder:not(.item-input).item-has-start-slot).label-floating.label-rtl, :host-context(.item-fill-outline.item-has-placeholder:not(.item-input).item-has-start-slot).label-floating.label-rtl,
:host-context(.item-fill-outline.item-has-value.item-has-start-slot).label-floating.label-rtl { :host-context(.item-fill-outline.item-has-value.item-has-start-slot).label-floating.label-rtl {
@include transform(translateX(calc(-1 * #{$item-md-fill-outline-label-translate-x})), translateY(-6px), scale(.75)); @include transform(translateX(calc(-1 * #{$item-md-fill-outline-label-translate-x})), translateY(-6px), scale(.75));
} }
:host-context(.ion-focused).label-stacked:not(.ion-color),
:host-context(.ion-focused).label-floating:not(.ion-color),
:host-context(.item-has-focus).label-stacked:not(.ion-color), :host-context(.item-has-focus).label-stacked:not(.ion-color),
:host-context(.item-has-focus).label-floating:not(.ion-color) { :host-context(.item-has-focus).label-floating:not(.ion-color) {
color: $label-md-text-color-focused; color: $label-md-text-color-focused;
} }
:host-context(.ion-focused.ion-color).label-stacked:not(.ion-color),
:host-context(.ion-focused.ion-color).label-floating:not(.ion-color),
:host-context(.item-has-focus.ion-color).label-stacked:not(.ion-color), :host-context(.item-has-focus.ion-color).label-stacked:not(.ion-color),
:host-context(.item-has-focus.ion-color).label-floating:not(.ion-color) { :host-context(.item-has-focus.ion-color).label-floating:not(.ion-color) {
color: #{current-color(contrast)}; color: #{current-color(contrast)};
} }
:host-context(.item-fill-solid.ion-focused.ion-color).label-stacked:not(.ion-color),
:host-context(.item-fill-solid.ion-focused.ion-color).label-floating:not(.ion-color),
:host-context(.item-fill-outline.ion-focused.ion-color).label-stacked:not(.ion-color),
:host-context(.item-fill-outline.ion-focused.ion-color).label-floating:not(.ion-color),
:host-context(.item-fill-solid.item-has-focus.ion-color).label-stacked:not(.ion-color), :host-context(.item-fill-solid.item-has-focus.ion-color).label-stacked:not(.ion-color),
:host-context(.item-fill-solid.item-has-focus.ion-color).label-floating:not(.ion-color), :host-context(.item-fill-solid.item-has-focus.ion-color).label-floating:not(.ion-color),
:host-context(.item-fill-outline.item-has-focus.ion-color).label-stacked:not(.ion-color), :host-context(.item-fill-outline.item-has-focus.ion-color).label-stacked:not(.ion-color),

View File

@ -32,7 +32,10 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
const results = getPopoverPosition(isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, trigger, ev); const results = getPopoverPosition(isRTL, contentWidth, contentHeight, arrowWidth, arrowHeight, reference, side, align, defaultPosition, trigger, ev);
const { originX, originY, top, left, bottom, checkSafeAreaLeft, checkSafeAreaRight, arrowTop, arrowLeft, addPopoverBottomClass } = calculateWindowAdjustment(side, results.top, results.left, POPOVER_IOS_BODY_PADDING, bodyWidth, bodyHeight, contentWidth, contentHeight, 25, results.originX, results.originY, results.referenceCoordinates, results.arrowTop, results.arrowLeft, arrowHeight); const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
const margin = size === 'cover' ? 0 : 25;
const { originX, originY, top, left, bottom, checkSafeAreaLeft, checkSafeAreaRight, arrowTop, arrowLeft, addPopoverBottomClass } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, margin, results.originX, results.originY, results.referenceCoordinates, results.arrowTop, results.arrowLeft, arrowHeight);
const baseAnimation = createAnimation(); const baseAnimation = createAnimation();
const backdropAnimation = createAnimation(); const backdropAnimation = createAnimation();

View File

@ -31,7 +31,9 @@ export const mdEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev); const results = getPopoverPosition(isRTL, contentWidth, contentHeight, 0, 0, reference, side, align, defaultPosition, trigger, ev);
const { originX, originY, top, left, bottom } = calculateWindowAdjustment(side, results.top, results.left, POPOVER_MD_BODY_PADDING, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates); const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING;
const { originX, originY, top, left, bottom } = calculateWindowAdjustment(side, results.top, results.left, padding, bodyWidth, bodyHeight, contentWidth, contentHeight, 0, results.originX, results.originY, results.referenceCoordinates);
const baseAnimation = createAnimation(); const baseAnimation = createAnimation();
const backdropAnimation = createAnimation(); const backdropAnimation = createAnimation();

View File

@ -1,6 +1,6 @@
# ion-select-popover # ion-select-popover
SelectPopover is an internal component that is used for create the popover interface, from a Select component. The select popover is an internal component that is used to create the popover interface from a select component.
<!-- Auto Generated Below --> <!-- Auto Generated Below -->

View File

@ -5,5 +5,5 @@ export interface SelectPopoverOption {
disabled: boolean; disabled: boolean;
checked: boolean; checked: boolean;
cssClass?: string | string[]; cssClass?: string | string[];
handler?: () => void; handler?: (value: any) => boolean | void | {[key: string]: any};
} }

View File

@ -0,0 +1,3 @@
@import "./select-popover";
@import "./select-popover.ios.vars";

View File

@ -0,0 +1,5 @@
@import "../../themes/ionic.globals.ios";
@import "../item/item.ios.vars";
// iOS Select Popover
// --------------------------------------------------

View File

@ -0,0 +1,25 @@
@import "./select-popover";
@import "./select-popover.md.vars";
ion-list ion-radio {
opacity: 0;
}
ion-item {
--inner-border-width: 0;
}
.item-radio-checked {
--background: #{ion-color(primary, base, 0.08)};
--background-focused: #{ion-color(primary, base)};
--background-focused-opacity: 0.2;
--background-hover: #{ion-color(primary, base)};
--background-hover-opacity: 0.12;
}
.item-checkbox-checked {
--background-activated: #{$item-md-color};
--background-focused: #{$item-md-color};
--background-hover: #{$item-md-color};
--color: #{ion-color(primary, base)};
}

View File

@ -0,0 +1,5 @@
@import "../../themes/ionic.globals.md";
@import "../item/item.md.vars";
// Material Design Select Popover
// --------------------------------------------------

View File

@ -1,10 +1,10 @@
@import "./select-popover.vars"; @import "./select-popover.vars";
:host ion-list { ion-list {
@include margin($select-popover-list-margin-top, $select-popover-list-margin-end, $select-popover-list-margin-bottom, $select-popover-list-margin-start); @include margin($select-popover-list-margin-top, $select-popover-list-margin-end, $select-popover-list-margin-bottom, $select-popover-list-margin-start);
} }
:host ion-list-header, ion-list-header,
:host ion-label { ion-label {
@include margin(0); @include margin(0);
} }

View File

@ -10,48 +10,133 @@ import { getClassMap } from '../../utils/theme';
*/ */
@Component({ @Component({
tag: 'ion-select-popover', tag: 'ion-select-popover',
styleUrl: 'select-popover.scss', styleUrls: {
ios: 'select-popover.ios.scss',
md: 'select-popover.md.scss'
},
scoped: true scoped: true
}) })
export class SelectPopover implements ComponentInterface { export class SelectPopover implements ComponentInterface {
/**
/** Header text for the popover */ * The header text of the popover
*/
@Prop() header?: string; @Prop() header?: string;
/** Subheader text for the popover */ /**
* The subheader text of the popover
*/
@Prop() subHeader?: string; @Prop() subHeader?: string;
/** Text for popover body */ /**
* The text content of the popover body
*/
@Prop() message?: string; @Prop() message?: string;
/** Array of options for the popover */ /**
* If true, the select accepts multiple values
*/
@Prop() multiple?: boolean;
/**
* An array of options for the popover
*/
@Prop() options: SelectPopoverOption[] = []; @Prop() options: SelectPopoverOption[] = [];
@Listen('ionChange') @Listen('ionChange')
onSelect(ev: any) { onSelect(ev: any) {
const option = this.options.find(o => o.value === ev.target.value); this.setChecked(ev);
if (option) { this.callOptionHandler(ev);
safeCall(option.handler); }
/**
* When an option is selected we need to get the value(s)
* of the selected option(s) and return it in the option
* handler
*/
private callOptionHandler(ev: any) {
const { options } = this;
const option = options.find(o => this.getValue(o.value) === ev.target.value);
const values = this.getValues(ev);
if (option && option.handler) {
safeCall(option.handler, values);
} }
} }
render() { /**
const checkedOption = this.options.find(o => o.checked); * This is required when selecting a radio that is already
const checkedValue = checkedOption ? checkedOption.value : undefined; * selected because it will not trigger the ionChange event
* but we still want to close the popover
*/
private rbClick(ev: any) {
this.callOptionHandler(ev);
}
private setChecked(ev: any): void {
const { multiple, options } = this;
const option = options.find(o => this.getValue(o.value) === ev.target.value);
// this is a popover with checkboxes (multiple value select)
// we need to set the checked value for this option
if (multiple && option) {
option.checked = ev.detail.checked;
}
}
private getValues(ev: any): any | any[] | null {
const { multiple, options } = this;
if (multiple) {
// this is a popover with checkboxes (multiple value select)
// return an array of all the checked values
return options.filter(o => o.checked).map(o => o.value);
}
// this is a popover with radio buttons (single value select)
// return the value that was clicked, otherwise undefined
const option = options.find(o => this.getValue(o.value) === ev.target.value);
return option ? option.value : undefined;
}
private getValue(value: any): any {
return typeof value === 'number' ? value.toString() : value;
}
renderOptions(options: SelectPopoverOption[]) {
const { multiple } = this;
switch (multiple) {
case true: return this.renderCheckboxOptions(options);
default: return this.renderRadioOptions(options);
}
}
renderCheckboxOptions(options: SelectPopoverOption[]) {
return ( return (
<Host class={getIonMode(this)}> options.map(option =>
<ion-list> <ion-item class={getClassMap(option.cssClass)}>
{this.header !== undefined && <ion-list-header>{this.header}</ion-list-header>} <ion-checkbox
{ (this.subHeader !== undefined || this.message !== undefined) && slot="start"
<ion-item> value={option.value}
<ion-label class="ion-text-wrap"> disabled={option.disabled}
{this.subHeader !== undefined && <h3>{this.subHeader}</h3>} checked={option.checked}
{this.message !== undefined && <p>{this.message}</p>} >
</ion-checkbox>
<ion-label>
{option.text}
</ion-label> </ion-label>
</ion-item> </ion-item>
)
)
} }
<ion-radio-group value={checkedValue}>
{this.options.map(option => renderRadioOptions(options: SelectPopoverOption[]) {
const checked = options.filter(o => o.checked).map(o => o.value)[0];
return (
<ion-radio-group value={checked}>
{options.map(option =>
<ion-item class={getClassMap(option.cssClass)}> <ion-item class={getClassMap(option.cssClass)}>
<ion-label> <ion-label>
{option.text} {option.text}
@ -59,11 +144,32 @@ export class SelectPopover implements ComponentInterface {
<ion-radio <ion-radio
value={option.value} value={option.value}
disabled={option.disabled} disabled={option.disabled}
onClick={ev => this.rbClick(ev)}
> >
</ion-radio> </ion-radio>
</ion-item> </ion-item>
)} )}
</ion-radio-group> </ion-radio-group>
)
}
render() {
const { header, message, options, subHeader } = this;
const hasSubHeaderOrMessage = subHeader !== undefined || message !== undefined;
return (
<Host class={getIonMode(this)}>
<ion-list>
{header !== undefined && <ion-list-header>{header}</ion-list-header>}
{ hasSubHeaderOrMessage &&
<ion-item>
<ion-label class="ion-text-wrap">
{subHeader !== undefined && <h3>{subHeader}</h3>}
{message !== undefined && <p>{message}</p>}
</ion-label>
</ion-item>
}
{this.renderOptions(options)}
</ion-list> </ion-list>
</Host> </Host>
); );

View File

@ -14,4 +14,6 @@
.select-icon { .select-icon {
width: 12px; width: 12px;
height: 18px; height: 18px;
opacity: .33;
} }

View File

@ -14,8 +14,41 @@
.select-icon { .select-icon {
width: 19px; width: 19px;
height: 19px; height: 19px;
transition: transform .15s cubic-bezier(.4, 0, .2, 1);
opacity: .55;
} }
:host-context(.item-label-floating) .select-icon { /**
* Adjust the arrow so that it appears in the middle
* of the item. If the item has fill="outline" then
* we should adjust the entire ion-select rather than
* just the outline so the selected value appears centered too.
*/
:host-context(.item-label-stacked) .select-icon,
:host-context(.item-label-floating:not(.item-fill-outline)) .select-icon,
:host-context(.item-label-floating.item-fill-outline) {
@include transform(translate3d(0, -9px, 0)); @include transform(translate3d(0, -9px, 0));
} }
:host-context(.item-has-focus) .select-icon {
@include transform(rotate(180deg));
}
/**
* Ensure that the translation we did
* above is preserved when we rotate the select icon.
*/
:host-context(.item-has-focus.item-label-stacked) .select-icon,
:host-context(.item-has-focus.item-label-floating:not(.item-fill-outline)) .select-icon {
@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);
opacity: 1;
}

View File

@ -66,8 +66,6 @@ button {
.select-icon { .select-icon {
position: relative; position: relative;
opacity: .33;
} }
.select-text { .select-text {

View File

@ -125,7 +125,8 @@ export class Select implements ComponentInterface {
@Watch('disabled') @Watch('disabled')
@Watch('placeholder') @Watch('placeholder')
disabledChanged() { @Watch('isExpanded')
styleChanged() {
this.emitStyle(); this.emitStyle();
} }
@ -177,28 +178,33 @@ export class Select implements ComponentInterface {
this.isExpanded = false; this.isExpanded = false;
this.setFocus(); this.setFocus();
}); });
if (this.interface === 'popover') {
await (overlay as HTMLIonPopoverElement).presentFromTrigger(event, true);
} else {
await overlay.present(); await overlay.present();
}
return overlay; return overlay;
} }
private createOverlay(ev?: UIEvent): Promise<OverlaySelect> { private createOverlay(ev?: UIEvent): Promise<OverlaySelect> {
let selectInterface = this.interface; let selectInterface = this.interface;
if ((selectInterface === 'action-sheet' || selectInterface === 'popover') && this.multiple) { if (selectInterface === 'action-sheet' && this.multiple) {
console.warn(`Select interface cannot be "${selectInterface}" with a multi-value select. Using the "alert" interface instead.`); console.warn(`Select interface cannot be "${selectInterface}" with a multi-value select. Using the "alert" interface instead.`);
selectInterface = 'alert'; selectInterface = 'alert';
} }
if (selectInterface === 'popover' && !ev) { if (selectInterface === 'popover' && !ev) {
console.warn('Select interface cannot be a "popover" without passing an event. Using the "alert" interface instead.'); console.warn(`Select interface cannot be a "${selectInterface}" without passing an event. Using the "alert" interface instead.`);
selectInterface = 'alert'; selectInterface = 'alert';
} }
if (selectInterface === 'popover') {
return this.openPopover(ev!);
}
if (selectInterface === 'action-sheet') { if (selectInterface === 'action-sheet') {
return this.openActionSheet(); return this.openActionSheet();
} }
if (selectInterface === 'popover') {
return this.openPopover(ev!);
}
return this.openAlert(); return this.openAlert();
} }
@ -291,10 +297,12 @@ export class Select implements ComponentInterface {
value, value,
checked: isOptionSelected(value, selectValue, this.compareWith), checked: isOptionSelected(value, selectValue, this.compareWith),
disabled: option.disabled, disabled: option.disabled,
handler: () => { handler: (selected: any) => {
this.value = value; this.value = selected;
if (!this.multiple) {
this.close(); this.close();
} }
}
}; };
}); });
@ -304,18 +312,43 @@ export class Select implements ComponentInterface {
private async openPopover(ev: UIEvent) { private async openPopover(ev: UIEvent) {
const interfaceOptions = this.interfaceOptions; const interfaceOptions = this.interfaceOptions;
const mode = getIonMode(this); const mode = getIonMode(this);
const showBackdrop = mode === 'md' ? false : true;
const multiple = this.multiple;
const value = this.value; const value = this.value;
let event: Event | CustomEvent = ev;
let size = 'auto';
const item = this.el.closest('ion-item');
// If the select is inside of an item containing a floating
// or stacked label then the popover should take up the
// full width of the item when it presents
if (item && (item.classList.contains('item-label-floating') || item.classList.contains('item-label-stacked'))) {
event = {
...ev,
detail: {
ionShadowTarget: item
}
}
size = 'cover';
}
const popoverOpts: PopoverOptions = { const popoverOpts: PopoverOptions = {
mode, mode,
event,
alignment: 'center',
size,
showBackdrop,
...interfaceOptions, ...interfaceOptions,
component: 'ion-select-popover', component: 'ion-select-popover',
cssClass: ['select-popover', interfaceOptions.cssClass], cssClass: ['select-popover', interfaceOptions.cssClass],
event: ev,
componentProps: { componentProps: {
header: interfaceOptions.header, header: interfaceOptions.header,
subHeader: interfaceOptions.subHeader, subHeader: interfaceOptions.subHeader,
message: interfaceOptions.message, message: interfaceOptions.message,
multiple,
value, value,
options: this.createPopoverOptions(this.childOpts, value) options: this.createPopoverOptions(this.childOpts, value)
} }
@ -411,11 +444,12 @@ export class Select implements ComponentInterface {
private emitStyle() { private emitStyle() {
this.ionStyle.emit({ this.ionStyle.emit({
'interactive': true, 'interactive': true,
'interactive-disabled': this.disabled,
'select': true, 'select': true,
'select-disabled': this.disabled,
'has-placeholder': this.placeholder !== undefined, 'has-placeholder': this.placeholder !== undefined,
'has-value': this.hasValue(), 'has-value': this.hasValue(),
'interactive-disabled': this.disabled, 'has-focus': this.isExpanded,
'select-disabled': this.disabled
}); });
} }
@ -423,6 +457,7 @@ export class Select implements ComponentInterface {
this.setFocus(); this.setFocus();
this.open(ev); this.open(ev);
} }
private onFocus = () => { private onFocus = () => {
this.ionFocus.emit(); this.ionFocus.emit();
} }

View File

@ -134,14 +134,11 @@
</ion-item> </ion-item>
<ion-item> <ion-item>
<ion-label>Gaming</ion-label> <ion-label>Favorite food</ion-label>
<ion-select name="gaming" ok-text="Okay" cancel-text="Dismiss" value="n64" interface="popover"> <ion-select name="food" interface="popover" value="steak">
<ion-select-option value="nes">NES</ion-select-option> <ion-select-option value="steak">Steak</ion-select-option>
<ion-select-option value="n64">Nintendo64</ion-select-option> <ion-select-option value="pizza">Pizza</ion-select-option>
<ion-select-option value="ps">PlayStation</ion-select-option> <ion-select-option value="tacos">Tacos</ion-select-option>
<ion-select-option value="genesis">Sega Genesis</ion-select-option>
<ion-select-option value="saturn">Sega Saturn</ion-select-option>
<ion-select-option value="snes">SNES</ion-select-option>
</ion-select> </ion-select>
</ion-item> </ion-item>
@ -178,7 +175,6 @@
</ion-list> </ion-list>
<ion-list> <ion-list>
<ion-list-header> <ion-list-header>
<ion-label> <ion-label>
@ -244,7 +240,7 @@
<ion-item> <ion-item>
<ion-label>Numbers</ion-label> <ion-label>Numbers</ion-label>
<ion-select id="numberSelect"> <ion-select id="numberSelect" multiple interface="popover">
<ion-select-option>0</ion-select-option> <ion-select-option>0</ion-select-option>
<ion-select-option>1</ion-select-option> <ion-select-option>1</ion-select-option>
<ion-select-option>2</ion-select-option> <ion-select-option>2</ion-select-option>
@ -254,6 +250,17 @@
</ion-select> </ion-select>
</ion-item> </ion-item>
<ion-item>
<ion-label>Toppings</ion-label>
<ion-select multiple interface="popover">
<ion-select-option>Extra cheese</ion-select-option>
<ion-select-option>Mushroom</ion-select-option>
<ion-select-option>Onion</ion-select-option>
<ion-select-option>Pepperoni</ion-select-option>
<ion-select-option>Sausage</ion-select-option>
</ion-select>
</ion-item>
<ion-item> <ion-item>
<ion-label>Disabled</ion-label> <ion-label>Disabled</ion-label>
<ion-select multiple value="text" disabled="true"> <ion-select multiple value="text" disabled="true">

View File

@ -0,0 +1,10 @@
import { newE2EPage } from '@stencil/core/testing';
test('select: spec', async () => {
const page = await newE2EPage({
url: '/src/components/select/test/spec?ionic:_testing=true'
});
const compare = await page.compareScreenshot();
expect(compare).toMatchScreenshot();
});

View File

@ -0,0 +1,455 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Select - Spec</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="../../../../../css/core.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script></head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>
Select - Spec
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<h1>Floating Selects</h1>
<div class="grid">
<div class="column">
<h2>Default</h2>
<ion-item>
<ion-label position="floating">Fruit</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</div>
<div class="column">
<h2>Default: Focused</h2>
<ion-item class="ion-focused">
<ion-label position="floating">Fruit</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</div>
<div class="column">
<h2>Filled</h2>
<ion-item fill="solid">
<ion-label position="floating">Fruit</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</div>
<div class="column">
<h2>Filled: Focused</h2>
<ion-item fill="solid" class="ion-focused">
<ion-label position="floating">Fruit</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</div>
<div class="column">
<h2>Outlined</h2>
<ion-item fill="outline">
<ion-label position="floating">Fruit</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</div>
<div class="column">
<h2>Outlined: Focused</h2>
<ion-item fill="outline" class="ion-focused">
<ion-label position="floating">Fruit</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</div>
</div>
<hr>
<h1>Stacked Selects</h1>
<div class="grid">
<div class="column">
<h2>Default</h2>
<ion-item>
<ion-label position="stacked">Fruit</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</div>
<div class="column">
<h2>Default: Focused</h2>
<ion-item class="ion-focused">
<ion-label position="stacked">Fruit</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</div>
<div class="column">
<h2>Filled</h2>
<ion-item fill="solid">
<ion-label position="stacked">Fruit</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</div>
<div class="column">
<h2>Filled: Focused</h2>
<ion-item fill="solid" class="ion-focused">
<ion-label position="stacked">Fruit</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</div>
<div class="column">
<h2>Outlined</h2>
<ion-item fill="outline">
<ion-label position="stacked">Fruit</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</div>
<div class="column">
<h2>Outlined: Focused</h2>
<ion-item fill="outline" class="ion-focused">
<ion-label position="stacked">Fruit</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</div>
</div>
<hr>
<h1>Inline Selects</h1>
<div class="grid">
<div class="column">
<h2>Default</h2>
<ion-item>
<ion-label>Fruit</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</div>
<div class="column">
<h2>Default: Focused</h2>
<ion-item class="ion-focused">
<ion-label>Fruit</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</div>
<div class="column">
<h2>Filled</h2>
<ion-item fill="solid">
<ion-label>Fruit</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</div>
<div class="column">
<h2>Filled: Focused</h2>
<ion-item fill="solid" class="ion-focused">
<ion-label>Fruit</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</div>
<div class="column">
<h2>Outlined</h2>
<ion-item fill="outline">
<ion-label>Fruit</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</div>
<div class="column">
<h2>Outlined: Focused</h2>
<ion-item fill="outline" class="ion-focused">
<ion-label>Fruit</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</div>
</div>
<hr>
<h1>Fixed Selects</h1>
<div class="grid">
<div class="column">
<h2>Default</h2>
<ion-item>
<ion-label position="fixed">Fruit</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</div>
<div class="column">
<h2>Default: Focused</h2>
<ion-item class="ion-focused">
<ion-label position="fixed">Fruit</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</div>
<div class="column">
<h2>Filled</h2>
<ion-item fill="solid">
<ion-label position="fixed">Fruit</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</div>
<div class="column">
<h2>Filled: Focused</h2>
<ion-item fill="solid" class="ion-focused">
<ion-label position="fixed">Fruit</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</div>
<div class="column">
<h2>Outlined</h2>
<ion-item fill="outline">
<ion-label position="fixed">Fruit</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</div>
<div class="column">
<h2>Outlined: Focused</h2>
<ion-item fill="outline" class="ion-focused">
<ion-label position="fixed">Fruit</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</div>
</div>
<hr>
<h1>Full Width Selects</h1>
<ion-list>
<ion-item>
<ion-label>Inline</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label position="fixed">Fixed</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label position="floating">Floating</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label position="stacked">Stacked</ion-label>
<ion-select interface="popover">
<ion-select-option></ion-select-option>
<ion-select-option value="apple">Apple</ion-select-option>
<ion-select-option value="orange">Orange</ion-select-option>
<ion-select-option value="banana">Banana</ion-select-option>
</ion-select>
</ion-item>
</ion-list>
<div class="margin-bottom-extra"></div>
</ion-content>
</ion-app>
<style>
h1 {
font-size: 14px;
color: #54575e;
margin: 25px 0 5px 25px;
}
h2 {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
font-weight: normal;
color: #a1a7b0;
margin-top: 10px;
margin-left: 5px;
}
hr {
border: none;
background: #eff1f3;
height: 1px;
margin: 18px 16px 25px 16px;
}
.grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
row-gap: 20px;
column-gap: 20px;
padding: 0 20px 20px;
}
.margin-bottom-extra {
margin-bottom: 300px;
}
</style>
</body>
</html>

View File

@ -50,11 +50,16 @@ export const startFocusVisible = (rootEl?: HTMLElement) => {
ref.addEventListener('touchstart', pointerDown); ref.addEventListener('touchstart', pointerDown);
ref.addEventListener('mousedown', pointerDown); ref.addEventListener('mousedown', pointerDown);
return () => { const destroy = () => {
ref.removeEventListener('keydown', onKeydown); ref.removeEventListener('keydown', onKeydown);
ref.removeEventListener('focusin', onFocusin); ref.removeEventListener('focusin', onFocusin);
ref.removeEventListener('focusout', onFocusout); ref.removeEventListener('focusout', onFocusout);
ref.removeEventListener('touchstart', pointerDown); ref.removeEventListener('touchstart', pointerDown);
ref.removeEventListener('mousedown', pointerDown); ref.removeEventListener('mousedown', pointerDown);
} }
return {
destroy,
setFocus
}
}; };

View File

@ -79,6 +79,21 @@ export const focusFirstDescendant = (ref: Element, overlay: HTMLIonOverlayElemen
if (firstInput) { if (firstInput) {
firstInput.focus(); firstInput.focus();
/**
* When programmatically focusing an element,
* the focus-visible utility will not run because
* it is expecting a keyboard event to have triggered this;
* however, there are times when we need to manually control
* this behavior so we call the `setFocus` method on ion-app
* which will let us explicitly set the elements to focus.
*/
if (firstInput.classList.contains('ion-focusable')) {
const app = overlay.closest('ion-app');
if (app) {
app.setFocus([firstInput]);
}
}
} else { } else {
// Focus overlay instead of letting focus escape // Focus overlay instead of letting focus escape
overlay.focus(); overlay.focus();

View File

@ -43,6 +43,10 @@ export const startTapClick = (config: Config) => {
} }
}; };
const onContextMenu = (ev: MouseEvent) => {
pointerUp(ev);
};
const cancelActive = () => { const cancelActive = () => {
clearTimeout(activeDefer); clearTimeout(activeDefer);
activeDefer = undefined; activeDefer = undefined;
@ -155,6 +159,8 @@ export const startTapClick = (config: Config) => {
doc.addEventListener('mousedown', onMouseDown, true); doc.addEventListener('mousedown', onMouseDown, true);
doc.addEventListener('mouseup', onMouseUp, true); doc.addEventListener('mouseup', onMouseUp, true);
doc.addEventListener('contextmenu', onContextMenu, true);
}; };
const getActivatableTarget = (ev: any): any => { const getActivatableTarget = (ev: any): any => {