mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 11:17:19 +08:00
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:
25
core/src/components.d.ts
vendored
25
core/src/components.d.ts
vendored
@ -208,6 +208,7 @@ export namespace Components {
|
||||
"translucent": boolean;
|
||||
}
|
||||
interface IonApp {
|
||||
"setFocus": (elements: HTMLElement[]) => Promise<void>;
|
||||
}
|
||||
interface IonAvatar {
|
||||
}
|
||||
@ -2358,19 +2359,23 @@ export namespace Components {
|
||||
}
|
||||
interface IonSelectPopover {
|
||||
/**
|
||||
* Header text for the popover
|
||||
* The header text of the popover
|
||||
*/
|
||||
"header"?: string;
|
||||
/**
|
||||
* Text for popover body
|
||||
* The text content of the popover body
|
||||
*/
|
||||
"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[];
|
||||
/**
|
||||
* Subheader text for the popover
|
||||
* The subheader text of the popover
|
||||
*/
|
||||
"subHeader"?: string;
|
||||
}
|
||||
@ -5946,19 +5951,23 @@ declare namespace LocalJSX {
|
||||
}
|
||||
interface IonSelectPopover {
|
||||
/**
|
||||
* Header text for the popover
|
||||
* The header text of the popover
|
||||
*/
|
||||
"header"?: string;
|
||||
/**
|
||||
* Text for popover body
|
||||
* The text content of the popover body
|
||||
*/
|
||||
"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[];
|
||||
/**
|
||||
* Subheader text for the popover
|
||||
* The subheader text of the popover
|
||||
*/
|
||||
"subHeader"?: string;
|
||||
}
|
||||
|
@ -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 { getIonMode } from '../../global/ionic-global';
|
||||
@ -9,6 +9,8 @@ import { isPlatform } from '../../utils/platform';
|
||||
styleUrl: 'app.scss',
|
||||
})
|
||||
export class App implements ComponentInterface {
|
||||
private focusVisible?: any;
|
||||
|
||||
@Element() el!: HTMLElement;
|
||||
|
||||
componentDidLoad() {
|
||||
@ -33,11 +35,28 @@ export class App implements ComponentInterface {
|
||||
if (typeof (window as any) !== 'undefined') {
|
||||
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() {
|
||||
const mode = getIonMode(this);
|
||||
return (
|
||||
|
@ -303,6 +303,19 @@ export default defineComponent({
|
||||
| `--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/)*
|
||||
|
@ -760,7 +760,7 @@ export class Datetime implements ComponentInterface {
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.clearFocusVisible = startFocusVisible(this.el);
|
||||
this.clearFocusVisible = startFocusVisible(this.el).destroy;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
|
@ -24,6 +24,7 @@
|
||||
--highlight-color-valid: #{$item-ios-input-highlight-color-valid};
|
||||
--highlight-color-invalid: #{$item-ios-input-highlight-color-invalid};
|
||||
--bottom-padding-start: 0px;
|
||||
|
||||
font-size: $item-ios-font-size;
|
||||
}
|
||||
|
||||
|
@ -99,6 +99,7 @@
|
||||
transition: none;
|
||||
}
|
||||
|
||||
:host(.item-fill-outline.ion-focused) .item-native,
|
||||
:host(.item-fill-outline.item-has-focus) .item-native {
|
||||
border-color: transparent;
|
||||
}
|
||||
@ -308,6 +309,8 @@
|
||||
--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-floating) {
|
||||
color: $label-md-text-color-focused;
|
||||
@ -347,13 +350,10 @@
|
||||
--border-color: #{$item-md-input-fill-border-color};
|
||||
}
|
||||
|
||||
:host(.item-fill-solid) .item-native:hover {
|
||||
--background: var(--background-hover);
|
||||
--border-color: #{$item-md-input-fill-border-color-hover};
|
||||
}
|
||||
|
||||
:host(.item-fill-solid.ion-focused) .item-native,
|
||||
:host(.item-fill-solid.item-has-focus) .item-native {
|
||||
--background: var(--background-focused);
|
||||
|
||||
border-bottom-color: var(--highlight-color-focused);
|
||||
}
|
||||
|
||||
@ -361,10 +361,20 @@
|
||||
@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
|
||||
// --------------------------------------------------
|
||||
|
||||
:host(.item-fill-outline) {
|
||||
--ripple-color: transparent;
|
||||
--background-focused: transparent;
|
||||
--background-hover: transparent;
|
||||
--border-color: #{$item-md-input-fill-border-color};
|
||||
--border-width: #{$item-md-border-bottom-width};
|
||||
|
||||
@ -379,10 +389,6 @@
|
||||
@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 {
|
||||
--inner-padding-start: 16px;
|
||||
|
||||
@ -393,14 +399,22 @@
|
||||
@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-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-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)) {
|
||||
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
|
||||
// --------------------------------------------------
|
||||
|
||||
|
@ -154,7 +154,7 @@
|
||||
// --------------------------------------------------
|
||||
|
||||
@media (any-hover: hover) {
|
||||
:host(.ion-activatable:hover) .item-native {
|
||||
:host(.ion-activatable:not(.ion-focused):hover) .item-native {
|
||||
color: var(--color-hover);
|
||||
|
||||
&::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)};
|
||||
|
||||
&::after {
|
||||
@ -173,6 +173,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Item: Disabled
|
||||
// --------------------------------------------------
|
||||
|
||||
@ -308,7 +309,11 @@ button, a {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
// Setting pointer-events to none allows the label
|
||||
// to be clicked to open the select interface
|
||||
::slotted(ion-label) {
|
||||
pointer-events: none;
|
||||
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@ -370,6 +375,8 @@ button, a {
|
||||
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-inner-highlight {
|
||||
transform: scaleX(1);
|
||||
@ -378,22 +385,27 @@ button, a {
|
||||
border-color: var(--highlight-background);
|
||||
}
|
||||
|
||||
:host(.ion-focused) .item-highlight,
|
||||
:host(.item-has-focus) .item-highlight {
|
||||
border-width: var(--full-highlight-height);
|
||||
|
||||
opacity: var(--show-full-highlight);
|
||||
}
|
||||
|
||||
:host(.ion-focused) .item-inner-highlight,
|
||||
:host(.item-has-focus) .item-inner-highlight {
|
||||
border-bottom-width: var(--inset-highlight-height);
|
||||
|
||||
opacity: var(--show-inset-highlight);
|
||||
}
|
||||
|
||||
:host(.ion-focused.item-fill-solid) .item-highlight,
|
||||
:host(.item-has-focus.item-fill-solid) .item-highlight {
|
||||
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:not(.item-fill-outline)) .item-highlight {
|
||||
border-top: none;
|
||||
@ -405,6 +417,7 @@ button, a {
|
||||
// Item Input Focused
|
||||
// --------------------------------------------------
|
||||
|
||||
:host(.item-interactive.ion-focused),
|
||||
:host(.item-interactive.item-has-focus),
|
||||
:host(.item-interactive.ion-touched.ion-invalid) {
|
||||
// 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
|
||||
// --------------------------------------------------
|
||||
|
||||
:host(.item-interactive.ion-focused),
|
||||
:host(.item-interactive.item-has-focus) {
|
||||
--highlight-background: var(--highlight-color-focused);
|
||||
}
|
||||
|
@ -47,6 +47,7 @@
|
||||
transform 150ms $label-md-transition-timing-function;
|
||||
}
|
||||
|
||||
:host-context(.ion-focused).label-floating,
|
||||
:host-context(.item-has-focus).label-floating,
|
||||
:host-context(.item-has-placeholder:not(.item-input)).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"`,
|
||||
* 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-placeholder:not(.item-input)).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-placeholder:not(.item-input).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));
|
||||
}
|
||||
|
||||
: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-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 {
|
||||
@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-floating:not(.ion-color) {
|
||||
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-floating:not(.ion-color) {
|
||||
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-floating:not(.ion-color),
|
||||
:host-context(.item-fill-outline.item-has-focus.ion-color).label-stacked:not(.ion-color),
|
||||
|
@ -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 { 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 backdropAnimation = createAnimation();
|
||||
|
@ -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 { 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 backdropAnimation = createAnimation();
|
||||
|
@ -1,6 +1,6 @@
|
||||
# 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 -->
|
||||
|
@ -5,5 +5,5 @@ export interface SelectPopoverOption {
|
||||
disabled: boolean;
|
||||
checked: boolean;
|
||||
cssClass?: string | string[];
|
||||
handler?: () => void;
|
||||
handler?: (value: any) => boolean | void | {[key: string]: any};
|
||||
}
|
||||
|
@ -0,0 +1,3 @@
|
||||
@import "./select-popover";
|
||||
@import "./select-popover.ios.vars";
|
||||
|
@ -0,0 +1,5 @@
|
||||
@import "../../themes/ionic.globals.ios";
|
||||
@import "../item/item.ios.vars";
|
||||
|
||||
// iOS Select Popover
|
||||
// --------------------------------------------------
|
25
core/src/components/select-popover/select-popover.md.scss
Normal file
25
core/src/components/select-popover/select-popover.md.scss
Normal 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)};
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
@import "../../themes/ionic.globals.md";
|
||||
@import "../item/item.md.vars";
|
||||
|
||||
// Material Design Select Popover
|
||||
// --------------------------------------------------
|
@ -1,10 +1,10 @@
|
||||
@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);
|
||||
}
|
||||
|
||||
:host ion-list-header,
|
||||
:host ion-label {
|
||||
ion-list-header,
|
||||
ion-label {
|
||||
@include margin(0);
|
||||
}
|
||||
|
@ -10,60 +10,166 @@ import { getClassMap } from '../../utils/theme';
|
||||
*/
|
||||
@Component({
|
||||
tag: 'ion-select-popover',
|
||||
styleUrl: 'select-popover.scss',
|
||||
styleUrls: {
|
||||
ios: 'select-popover.ios.scss',
|
||||
md: 'select-popover.md.scss'
|
||||
},
|
||||
scoped: true
|
||||
})
|
||||
export class SelectPopover implements ComponentInterface {
|
||||
|
||||
/** Header text for the popover */
|
||||
/**
|
||||
* The header text of the popover
|
||||
*/
|
||||
@Prop() header?: string;
|
||||
|
||||
/** Subheader text for the popover */
|
||||
/**
|
||||
* The subheader text of the popover
|
||||
*/
|
||||
@Prop() subHeader?: string;
|
||||
|
||||
/** Text for popover body */
|
||||
/**
|
||||
* The text content of the popover body
|
||||
*/
|
||||
@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[] = [];
|
||||
|
||||
@Listen('ionChange')
|
||||
onSelect(ev: any) {
|
||||
const option = this.options.find(o => o.value === ev.target.value);
|
||||
if (option) {
|
||||
safeCall(option.handler);
|
||||
this.setChecked(ev);
|
||||
this.callOptionHandler(ev);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is required when selecting a radio that is already
|
||||
* 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 (
|
||||
options.map(option =>
|
||||
<ion-item class={getClassMap(option.cssClass)}>
|
||||
<ion-checkbox
|
||||
slot="start"
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
checked={option.checked}
|
||||
>
|
||||
</ion-checkbox>
|
||||
<ion-label>
|
||||
{option.text}
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
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-label>
|
||||
{option.text}
|
||||
</ion-label>
|
||||
<ion-radio
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
onClick={ev => this.rbClick(ev)}
|
||||
>
|
||||
</ion-radio>
|
||||
</ion-item>
|
||||
)}
|
||||
</ion-radio-group>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const checkedOption = this.options.find(o => o.checked);
|
||||
const checkedValue = checkedOption ? checkedOption.value : undefined;
|
||||
const { header, message, options, subHeader } = this;
|
||||
const hasSubHeaderOrMessage = subHeader !== undefined || message !== undefined;
|
||||
|
||||
return (
|
||||
<Host class={getIonMode(this)}>
|
||||
<ion-list>
|
||||
{this.header !== undefined && <ion-list-header>{this.header}</ion-list-header>}
|
||||
{ (this.subHeader !== undefined || this.message !== undefined) &&
|
||||
{header !== undefined && <ion-list-header>{header}</ion-list-header>}
|
||||
{ hasSubHeaderOrMessage &&
|
||||
<ion-item>
|
||||
<ion-label class="ion-text-wrap">
|
||||
{this.subHeader !== undefined && <h3>{this.subHeader}</h3>}
|
||||
{this.message !== undefined && <p>{this.message}</p>}
|
||||
{subHeader !== undefined && <h3>{subHeader}</h3>}
|
||||
{message !== undefined && <p>{message}</p>}
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
}
|
||||
<ion-radio-group value={checkedValue}>
|
||||
{this.options.map(option =>
|
||||
<ion-item class={getClassMap(option.cssClass)}>
|
||||
<ion-label>
|
||||
{option.text}
|
||||
</ion-label>
|
||||
<ion-radio
|
||||
value={option.value}
|
||||
disabled={option.disabled}
|
||||
>
|
||||
</ion-radio>
|
||||
</ion-item>
|
||||
)}
|
||||
</ion-radio-group>
|
||||
{this.renderOptions(options)}
|
||||
</ion-list>
|
||||
</Host>
|
||||
);
|
||||
|
@ -14,4 +14,6 @@
|
||||
.select-icon {
|
||||
width: 12px;
|
||||
height: 18px;
|
||||
|
||||
opacity: .33;
|
||||
}
|
||||
|
@ -14,8 +14,41 @@
|
||||
.select-icon {
|
||||
width: 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));
|
||||
}
|
||||
|
||||
: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;
|
||||
}
|
||||
|
@ -66,8 +66,6 @@ button {
|
||||
|
||||
.select-icon {
|
||||
position: relative;
|
||||
|
||||
opacity: .33;
|
||||
}
|
||||
|
||||
.select-text {
|
||||
|
@ -125,7 +125,8 @@ export class Select implements ComponentInterface {
|
||||
|
||||
@Watch('disabled')
|
||||
@Watch('placeholder')
|
||||
disabledChanged() {
|
||||
@Watch('isExpanded')
|
||||
styleChanged() {
|
||||
this.emitStyle();
|
||||
}
|
||||
|
||||
@ -177,28 +178,33 @@ export class Select implements ComponentInterface {
|
||||
this.isExpanded = false;
|
||||
this.setFocus();
|
||||
});
|
||||
await overlay.present();
|
||||
|
||||
if (this.interface === 'popover') {
|
||||
await (overlay as HTMLIonPopoverElement).presentFromTrigger(event, true);
|
||||
} else {
|
||||
await overlay.present();
|
||||
}
|
||||
return overlay;
|
||||
}
|
||||
|
||||
private createOverlay(ev?: UIEvent): Promise<OverlaySelect> {
|
||||
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.`);
|
||||
selectInterface = 'alert';
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
if (selectInterface === 'popover') {
|
||||
return this.openPopover(ev!);
|
||||
}
|
||||
if (selectInterface === 'action-sheet') {
|
||||
return this.openActionSheet();
|
||||
}
|
||||
if (selectInterface === 'popover') {
|
||||
return this.openPopover(ev!);
|
||||
}
|
||||
return this.openAlert();
|
||||
}
|
||||
|
||||
@ -291,9 +297,11 @@ export class Select implements ComponentInterface {
|
||||
value,
|
||||
checked: isOptionSelected(value, selectValue, this.compareWith),
|
||||
disabled: option.disabled,
|
||||
handler: () => {
|
||||
this.value = value;
|
||||
this.close();
|
||||
handler: (selected: any) => {
|
||||
this.value = selected;
|
||||
if (!this.multiple) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
@ -304,18 +312,43 @@ export class Select implements ComponentInterface {
|
||||
private async openPopover(ev: UIEvent) {
|
||||
const interfaceOptions = this.interfaceOptions;
|
||||
const mode = getIonMode(this);
|
||||
const showBackdrop = mode === 'md' ? false : true;
|
||||
const multiple = this.multiple;
|
||||
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 = {
|
||||
mode,
|
||||
event,
|
||||
alignment: 'center',
|
||||
size,
|
||||
showBackdrop,
|
||||
...interfaceOptions,
|
||||
|
||||
component: 'ion-select-popover',
|
||||
cssClass: ['select-popover', interfaceOptions.cssClass],
|
||||
event: ev,
|
||||
componentProps: {
|
||||
header: interfaceOptions.header,
|
||||
subHeader: interfaceOptions.subHeader,
|
||||
message: interfaceOptions.message,
|
||||
multiple,
|
||||
value,
|
||||
options: this.createPopoverOptions(this.childOpts, value)
|
||||
}
|
||||
@ -411,11 +444,12 @@ export class Select implements ComponentInterface {
|
||||
private emitStyle() {
|
||||
this.ionStyle.emit({
|
||||
'interactive': true,
|
||||
'interactive-disabled': this.disabled,
|
||||
'select': true,
|
||||
'select-disabled': this.disabled,
|
||||
'has-placeholder': this.placeholder !== undefined,
|
||||
'has-value': this.hasValue(),
|
||||
'interactive-disabled': this.disabled,
|
||||
'select-disabled': this.disabled
|
||||
'has-focus': this.isExpanded,
|
||||
});
|
||||
}
|
||||
|
||||
@ -423,6 +457,7 @@ export class Select implements ComponentInterface {
|
||||
this.setFocus();
|
||||
this.open(ev);
|
||||
}
|
||||
|
||||
private onFocus = () => {
|
||||
this.ionFocus.emit();
|
||||
}
|
||||
|
@ -134,14 +134,11 @@
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Gaming</ion-label>
|
||||
<ion-select name="gaming" ok-text="Okay" cancel-text="Dismiss" value="n64" interface="popover">
|
||||
<ion-select-option value="nes">NES</ion-select-option>
|
||||
<ion-select-option value="n64">Nintendo64</ion-select-option>
|
||||
<ion-select-option value="ps">PlayStation</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-label>Favorite food</ion-label>
|
||||
<ion-select name="food" interface="popover" value="steak">
|
||||
<ion-select-option value="steak">Steak</ion-select-option>
|
||||
<ion-select-option value="pizza">Pizza</ion-select-option>
|
||||
<ion-select-option value="tacos">Tacos</ion-select-option>
|
||||
</ion-select>
|
||||
</ion-item>
|
||||
|
||||
@ -178,7 +175,6 @@
|
||||
|
||||
</ion-list>
|
||||
|
||||
|
||||
<ion-list>
|
||||
<ion-list-header>
|
||||
<ion-label>
|
||||
@ -244,7 +240,7 @@
|
||||
|
||||
<ion-item>
|
||||
<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>1</ion-select-option>
|
||||
<ion-select-option>2</ion-select-option>
|
||||
@ -254,6 +250,17 @@
|
||||
</ion-select>
|
||||
</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-label>Disabled</ion-label>
|
||||
<ion-select multiple value="text" disabled="true">
|
||||
|
10
core/src/components/select/test/spec/e2e.ts
Normal file
10
core/src/components/select/test/spec/e2e.ts
Normal 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();
|
||||
});
|
455
core/src/components/select/test/spec/index.html
Normal file
455
core/src/components/select/test/spec/index.html
Normal 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>
|
@ -50,11 +50,16 @@ export const startFocusVisible = (rootEl?: HTMLElement) => {
|
||||
ref.addEventListener('touchstart', pointerDown);
|
||||
ref.addEventListener('mousedown', pointerDown);
|
||||
|
||||
return () => {
|
||||
const destroy = () => {
|
||||
ref.removeEventListener('keydown', onKeydown);
|
||||
ref.removeEventListener('focusin', onFocusin);
|
||||
ref.removeEventListener('focusout', onFocusout);
|
||||
ref.removeEventListener('touchstart', pointerDown);
|
||||
ref.removeEventListener('mousedown', pointerDown);
|
||||
}
|
||||
|
||||
return {
|
||||
destroy,
|
||||
setFocus
|
||||
}
|
||||
};
|
||||
|
@ -79,6 +79,21 @@ export const focusFirstDescendant = (ref: Element, overlay: HTMLIonOverlayElemen
|
||||
|
||||
if (firstInput) {
|
||||
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 {
|
||||
// Focus overlay instead of letting focus escape
|
||||
overlay.focus();
|
||||
|
@ -43,6 +43,10 @@ export const startTapClick = (config: Config) => {
|
||||
}
|
||||
};
|
||||
|
||||
const onContextMenu = (ev: MouseEvent) => {
|
||||
pointerUp(ev);
|
||||
};
|
||||
|
||||
const cancelActive = () => {
|
||||
clearTimeout(activeDefer);
|
||||
activeDefer = undefined;
|
||||
@ -155,6 +159,8 @@ export const startTapClick = (config: Config) => {
|
||||
|
||||
doc.addEventListener('mousedown', onMouseDown, true);
|
||||
doc.addEventListener('mouseup', onMouseUp, true);
|
||||
|
||||
doc.addEventListener('contextmenu', onContextMenu, true);
|
||||
};
|
||||
|
||||
const getActivatableTarget = (ev: any): any => {
|
||||
|
Reference in New Issue
Block a user