Compare commits
13 Commits
cb/module-
...
5580-test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
629d862c54 | ||
|
|
2c773ed0e6 | ||
|
|
b1fc67227c | ||
|
|
75ee951ce8 | ||
|
|
2f3f9dc9ca | ||
|
|
b68c93d55d | ||
|
|
eace6425a2 | ||
|
|
1aeb19403b | ||
|
|
9d0834b201 | ||
|
|
7b21bd40a6 | ||
|
|
0b469646b2 | ||
|
|
5e47412e1f | ||
|
|
cc45e2220b |
4
.github/CODEOWNERS
vendored
@@ -51,8 +51,8 @@
|
||||
/core/src/components/nav/ @sean-perkins
|
||||
/core/src/components/nav-link/ @sean-perkins
|
||||
|
||||
/core/src/components/picker-internal/ @liamdebeasi
|
||||
/core/src/components/picker-column-internal/ @liamdebeasi
|
||||
/core/src/components/picker/ @liamdebeasi
|
||||
/core/src/components/picker-column/ @liamdebeasi
|
||||
|
||||
/core/src/components/radio/ @amandaejohnston
|
||||
/core/src/components/radio-group/ @amandaejohnston
|
||||
|
||||
@@ -17,6 +17,7 @@ This is a comprehensive list of the breaking changes introduced in the major ver
|
||||
- [Components](#version-8x-components)
|
||||
- [Content](#version-8x-content)
|
||||
- [Datetime](#version-8x-datetime)
|
||||
- [Picker](#version-8x-picker)
|
||||
|
||||
<h2 id="version-8x-browser-platform-support">Browser and Platform Support</h2>
|
||||
|
||||
@@ -59,4 +60,9 @@ This section details the desktop browser, JavaScript framework, and mobile platf
|
||||
|
||||
+ background: red;
|
||||
}
|
||||
```
|
||||
```
|
||||
<h2 id="version-8x-picker">Picker</h2>
|
||||
|
||||
- `ion-picker` and `ion-picker-column` have been renamed to `ion-picker-legacy` and `ion-picker-legacy-column`, respectively. This change was made to accommodate the new inline picker component while allowing developers to continue to use the legacy picker during this migration period.
|
||||
- Only the component names have been changed. Usages such as `ion-picker` or `IonPicker` should be changed to `ion-picker-legacy` and `IonPickerLegacy`, respectively.
|
||||
- Non-component usages such as `pickerController` or `useIonPicker` remain unchanged. The new picker displays inline with your page content and does not have equivalents for these non-component usages.
|
||||
97
core/api.txt
@@ -906,47 +906,64 @@ ion-note,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "second
|
||||
ion-note,prop,mode,"ios" | "md",undefined,false,false
|
||||
ion-note,css-prop,--color
|
||||
|
||||
ion-picker,scoped
|
||||
ion-picker,prop,animated,boolean,true,false,false
|
||||
ion-picker,prop,backdropDismiss,boolean,true,false,false
|
||||
ion-picker,prop,buttons,PickerButton[],[],false,false
|
||||
ion-picker,prop,columns,PickerColumn[],[],false,false
|
||||
ion-picker,prop,cssClass,string | string[] | undefined,undefined,false,false
|
||||
ion-picker,prop,duration,number,0,false,false
|
||||
ion-picker,prop,enterAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
|
||||
ion-picker,prop,htmlAttributes,undefined | { [key: string]: any; },undefined,false,false
|
||||
ion-picker,prop,isOpen,boolean,false,false,false
|
||||
ion-picker,prop,keyboardClose,boolean,true,false,false
|
||||
ion-picker,prop,leaveAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
|
||||
ion-picker,shadow
|
||||
ion-picker,prop,mode,"ios" | "md",undefined,false,false
|
||||
ion-picker,prop,showBackdrop,boolean,true,false,false
|
||||
ion-picker,prop,trigger,string | undefined,undefined,false,false
|
||||
ion-picker,method,dismiss,dismiss(data?: any, role?: string) => Promise<boolean>
|
||||
ion-picker,method,getColumn,getColumn(name: string) => Promise<PickerColumn | undefined>
|
||||
ion-picker,method,onDidDismiss,onDidDismiss<T = any>() => Promise<OverlayEventDetail<T>>
|
||||
ion-picker,method,onWillDismiss,onWillDismiss<T = any>() => Promise<OverlayEventDetail<T>>
|
||||
ion-picker,method,present,present() => Promise<void>
|
||||
ion-picker,event,didDismiss,OverlayEventDetail<any>,true
|
||||
ion-picker,event,didPresent,void,true
|
||||
ion-picker,event,ionPickerDidDismiss,OverlayEventDetail<any>,true
|
||||
ion-picker,event,ionPickerDidPresent,void,true
|
||||
ion-picker,event,ionPickerWillDismiss,OverlayEventDetail<any>,true
|
||||
ion-picker,event,ionPickerWillPresent,void,true
|
||||
ion-picker,event,willDismiss,OverlayEventDetail<any>,true
|
||||
ion-picker,event,willPresent,void,true
|
||||
ion-picker,css-prop,--backdrop-opacity
|
||||
ion-picker,css-prop,--background
|
||||
ion-picker,css-prop,--background-rgb
|
||||
ion-picker,css-prop,--border-color
|
||||
ion-picker,css-prop,--border-radius
|
||||
ion-picker,css-prop,--border-style
|
||||
ion-picker,css-prop,--border-width
|
||||
ion-picker,css-prop,--height
|
||||
ion-picker,css-prop,--max-height
|
||||
ion-picker,css-prop,--max-width
|
||||
ion-picker,css-prop,--min-height
|
||||
ion-picker,css-prop,--min-width
|
||||
ion-picker,css-prop,--width
|
||||
ion-picker,event,ionInputModeChange,PickerChangeEventDetail,true
|
||||
|
||||
ion-picker-column,shadow
|
||||
ion-picker-column,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,'primary',false,true
|
||||
ion-picker-column,prop,disabled,boolean,false,false,false
|
||||
ion-picker-column,prop,items,PickerColumnItem[],[],false,false
|
||||
ion-picker-column,prop,mode,"ios" | "md",undefined,false,false
|
||||
ion-picker-column,prop,value,number | string | undefined,undefined,false,false
|
||||
ion-picker-column,event,ionChange,PickerColumnItem,true
|
||||
|
||||
ion-picker-column-option,shadow
|
||||
ion-picker-column-option,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,'primary',false,true
|
||||
ion-picker-column-option,prop,disabled,boolean,false,false,false
|
||||
ion-picker-column-option,prop,value,any,undefined,false,false
|
||||
|
||||
ion-picker-legacy,scoped
|
||||
ion-picker-legacy,prop,animated,boolean,true,false,false
|
||||
ion-picker-legacy,prop,backdropDismiss,boolean,true,false,false
|
||||
ion-picker-legacy,prop,buttons,PickerButton[],[],false,false
|
||||
ion-picker-legacy,prop,columns,PickerColumn[],[],false,false
|
||||
ion-picker-legacy,prop,cssClass,string | string[] | undefined,undefined,false,false
|
||||
ion-picker-legacy,prop,duration,number,0,false,false
|
||||
ion-picker-legacy,prop,enterAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
|
||||
ion-picker-legacy,prop,htmlAttributes,undefined | { [key: string]: any; },undefined,false,false
|
||||
ion-picker-legacy,prop,isOpen,boolean,false,false,false
|
||||
ion-picker-legacy,prop,keyboardClose,boolean,true,false,false
|
||||
ion-picker-legacy,prop,leaveAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
|
||||
ion-picker-legacy,prop,mode,"ios" | "md",undefined,false,false
|
||||
ion-picker-legacy,prop,showBackdrop,boolean,true,false,false
|
||||
ion-picker-legacy,prop,trigger,string | undefined,undefined,false,false
|
||||
ion-picker-legacy,method,dismiss,dismiss(data?: any, role?: string) => Promise<boolean>
|
||||
ion-picker-legacy,method,getColumn,getColumn(name: string) => Promise<PickerColumn | undefined>
|
||||
ion-picker-legacy,method,onDidDismiss,onDidDismiss<T = any>() => Promise<OverlayEventDetail<T>>
|
||||
ion-picker-legacy,method,onWillDismiss,onWillDismiss<T = any>() => Promise<OverlayEventDetail<T>>
|
||||
ion-picker-legacy,method,present,present() => Promise<void>
|
||||
ion-picker-legacy,event,didDismiss,OverlayEventDetail<any>,true
|
||||
ion-picker-legacy,event,didPresent,void,true
|
||||
ion-picker-legacy,event,ionPickerDidDismiss,OverlayEventDetail<any>,true
|
||||
ion-picker-legacy,event,ionPickerDidPresent,void,true
|
||||
ion-picker-legacy,event,ionPickerWillDismiss,OverlayEventDetail<any>,true
|
||||
ion-picker-legacy,event,ionPickerWillPresent,void,true
|
||||
ion-picker-legacy,event,willDismiss,OverlayEventDetail<any>,true
|
||||
ion-picker-legacy,event,willPresent,void,true
|
||||
ion-picker-legacy,css-prop,--backdrop-opacity
|
||||
ion-picker-legacy,css-prop,--background
|
||||
ion-picker-legacy,css-prop,--background-rgb
|
||||
ion-picker-legacy,css-prop,--border-color
|
||||
ion-picker-legacy,css-prop,--border-radius
|
||||
ion-picker-legacy,css-prop,--border-style
|
||||
ion-picker-legacy,css-prop,--border-width
|
||||
ion-picker-legacy,css-prop,--height
|
||||
ion-picker-legacy,css-prop,--max-height
|
||||
ion-picker-legacy,css-prop,--max-width
|
||||
ion-picker-legacy,css-prop,--min-height
|
||||
ion-picker-legacy,css-prop,--min-width
|
||||
ion-picker-legacy,css-prop,--width
|
||||
|
||||
ion-popover,shadow
|
||||
ion-popover,prop,alignment,"center" | "end" | "start" | undefined,undefined,false,false
|
||||
|
||||
305
core/src/components.d.ts
vendored
@@ -23,9 +23,9 @@ import { MenuChangeEventDetail, Side } from "./components/menu/menu-interface";
|
||||
import { ModalBreakpointChangeEventDetail, ModalHandleBehavior } from "./components/modal/modal-interface";
|
||||
import { NavComponent, NavComponentWithProps, NavOptions, RouterOutletOptions, SwipeGestureHandler, TransitionDoneFn, TransitionInstruction } from "./components/nav/nav-interface";
|
||||
import { ViewController } from "./components/nav/view-controller";
|
||||
import { PickerButton, PickerColumn } from "./components/picker/picker-interface";
|
||||
import { PickerColumnItem } from "./components/picker-column-internal/picker-column-internal-interfaces";
|
||||
import { PickerInternalChangeEventDetail } from "./components/picker-internal/picker-internal-interfaces";
|
||||
import { PickerChangeEventDetail } from "./components/picker/picker-interfaces";
|
||||
import { PickerColumnItem } from "./components/picker-column/picker-column-interfaces";
|
||||
import { PickerButton, PickerColumn } from "./components/picker-legacy/picker-interface";
|
||||
import { PopoverSize, PositionAlign, PositionReference, PositionSide, TriggerAction } from "./components/popover/popover-interface";
|
||||
import { RadioGroupChangeEventDetail } from "./components/radio-group/radio-group-interface";
|
||||
import { PinFormatter, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue } from "./components/range/range-interface";
|
||||
@@ -59,9 +59,9 @@ export { MenuChangeEventDetail, Side } from "./components/menu/menu-interface";
|
||||
export { ModalBreakpointChangeEventDetail, ModalHandleBehavior } from "./components/modal/modal-interface";
|
||||
export { NavComponent, NavComponentWithProps, NavOptions, RouterOutletOptions, SwipeGestureHandler, TransitionDoneFn, TransitionInstruction } from "./components/nav/nav-interface";
|
||||
export { ViewController } from "./components/nav/view-controller";
|
||||
export { PickerButton, PickerColumn } from "./components/picker/picker-interface";
|
||||
export { PickerColumnItem } from "./components/picker-column-internal/picker-column-internal-interfaces";
|
||||
export { PickerInternalChangeEventDetail } from "./components/picker-internal/picker-internal-interfaces";
|
||||
export { PickerChangeEventDetail } from "./components/picker/picker-interfaces";
|
||||
export { PickerColumnItem } from "./components/picker-column/picker-column-interfaces";
|
||||
export { PickerButton, PickerColumn } from "./components/picker-legacy/picker-interface";
|
||||
export { PopoverSize, PositionAlign, PositionReference, PositionSide, TriggerAction } from "./components/popover/popover-interface";
|
||||
export { RadioGroupChangeEventDetail } from "./components/radio-group/radio-group-interface";
|
||||
export { PinFormatter, RangeChangeEventDetail, RangeKnobMoveEndEventDetail, RangeKnobMoveStartEventDetail, RangeValue } from "./components/range/range-interface";
|
||||
@@ -1949,6 +1949,58 @@ export namespace Components {
|
||||
"mode"?: "ios" | "md";
|
||||
}
|
||||
interface IonPicker {
|
||||
"exitInputMode": () => Promise<void>;
|
||||
/**
|
||||
* The mode determines which platform styles to use.
|
||||
*/
|
||||
"mode"?: "ios" | "md";
|
||||
}
|
||||
interface IonPickerColumn {
|
||||
/**
|
||||
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
|
||||
*/
|
||||
"color"?: Color;
|
||||
/**
|
||||
* If `true`, the user cannot interact with the picker.
|
||||
*/
|
||||
"disabled": boolean;
|
||||
/**
|
||||
* A list of options to be displayed in the picker
|
||||
*/
|
||||
"items": PickerColumnItem[];
|
||||
/**
|
||||
* The mode determines which platform styles to use.
|
||||
*/
|
||||
"mode"?: "ios" | "md";
|
||||
/**
|
||||
* If `true`, tapping the picker will reveal a number input keyboard that lets the user type in values for each picker column. This is useful when working with time pickers.
|
||||
*/
|
||||
"numericInput": boolean;
|
||||
"scrollActiveItemIntoView": () => Promise<void>;
|
||||
/**
|
||||
* Sets the value prop and fires the ionChange event. This is used when we need to fire ionChange from user-generated events that cannot be caught with normal input/change event listeners.
|
||||
*/
|
||||
"setValue": (value?: string | number) => Promise<void>;
|
||||
/**
|
||||
* The selected option in the picker.
|
||||
*/
|
||||
"value"?: string | number;
|
||||
}
|
||||
interface IonPickerColumnOption {
|
||||
/**
|
||||
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
|
||||
*/
|
||||
"color"?: Color;
|
||||
/**
|
||||
* If `true`, the user cannot interact with the picker column option.
|
||||
*/
|
||||
"disabled": boolean;
|
||||
/**
|
||||
* The text value of the option.
|
||||
*/
|
||||
"value"?: any | null;
|
||||
}
|
||||
interface IonPickerLegacy {
|
||||
/**
|
||||
* If `true`, the picker will animate.
|
||||
*/
|
||||
@@ -2032,50 +2084,12 @@ export namespace Components {
|
||||
*/
|
||||
"trigger": string | undefined;
|
||||
}
|
||||
interface IonPickerColumn {
|
||||
interface IonPickerLegacyColumn {
|
||||
/**
|
||||
* Picker column data
|
||||
*/
|
||||
"col": PickerColumn;
|
||||
}
|
||||
interface IonPickerColumnInternal {
|
||||
/**
|
||||
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
|
||||
*/
|
||||
"color"?: Color;
|
||||
/**
|
||||
* If `true`, the user cannot interact with the picker.
|
||||
*/
|
||||
"disabled": boolean;
|
||||
/**
|
||||
* A list of options to be displayed in the picker
|
||||
*/
|
||||
"items": PickerColumnItem[];
|
||||
/**
|
||||
* The mode determines which platform styles to use.
|
||||
*/
|
||||
"mode"?: "ios" | "md";
|
||||
/**
|
||||
* If `true`, tapping the picker will reveal a number input keyboard that lets the user type in values for each picker column. This is useful when working with time pickers.
|
||||
*/
|
||||
"numericInput": boolean;
|
||||
"scrollActiveItemIntoView": () => Promise<void>;
|
||||
/**
|
||||
* Sets the value prop and fires the ionChange event. This is used when we need to fire ionChange from user-generated events that cannot be caught with normal input/change event listeners.
|
||||
*/
|
||||
"setValue": (value?: string | number) => Promise<void>;
|
||||
/**
|
||||
* The selected option in the picker.
|
||||
*/
|
||||
"value"?: string | number;
|
||||
}
|
||||
interface IonPickerInternal {
|
||||
"exitInputMode": () => Promise<void>;
|
||||
/**
|
||||
* The mode determines which platform styles to use.
|
||||
*/
|
||||
"mode"?: "ios" | "md";
|
||||
}
|
||||
interface IonPopover {
|
||||
/**
|
||||
* Describes how to align the popover content with the `reference` point. Defaults to `"center"` for `ios` mode, and `"start"` for `md` mode.
|
||||
@@ -3330,13 +3344,13 @@ export interface IonPickerColumnCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLIonPickerColumnElement;
|
||||
}
|
||||
export interface IonPickerColumnInternalCustomEvent<T> extends CustomEvent<T> {
|
||||
export interface IonPickerLegacyCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLIonPickerColumnInternalElement;
|
||||
target: HTMLIonPickerLegacyElement;
|
||||
}
|
||||
export interface IonPickerInternalCustomEvent<T> extends CustomEvent<T> {
|
||||
export interface IonPickerLegacyColumnCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
target: HTMLIonPickerInternalElement;
|
||||
target: HTMLIonPickerLegacyColumnElement;
|
||||
}
|
||||
export interface IonPopoverCustomEvent<T> extends CustomEvent<T> {
|
||||
detail: T;
|
||||
@@ -4020,14 +4034,7 @@ declare global {
|
||||
new (): HTMLIonNoteElement;
|
||||
};
|
||||
interface HTMLIonPickerElementEventMap {
|
||||
"ionPickerDidPresent": void;
|
||||
"ionPickerWillPresent": void;
|
||||
"ionPickerWillDismiss": OverlayEventDetail;
|
||||
"ionPickerDidDismiss": OverlayEventDetail;
|
||||
"didPresent": void;
|
||||
"willPresent": void;
|
||||
"willDismiss": OverlayEventDetail;
|
||||
"didDismiss": OverlayEventDetail;
|
||||
"ionInputModeChange": PickerChangeEventDetail;
|
||||
}
|
||||
interface HTMLIonPickerElement extends Components.IonPicker, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLIonPickerElementEventMap>(type: K, listener: (this: HTMLIonPickerElement, ev: IonPickerCustomEvent<HTMLIonPickerElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
@@ -4044,7 +4051,7 @@ declare global {
|
||||
new (): HTMLIonPickerElement;
|
||||
};
|
||||
interface HTMLIonPickerColumnElementEventMap {
|
||||
"ionPickerColChange": PickerColumn;
|
||||
"ionChange": string | number | undefined;
|
||||
}
|
||||
interface HTMLIonPickerColumnElement extends Components.IonPickerColumn, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLIonPickerColumnElementEventMap>(type: K, listener: (this: HTMLIonPickerColumnElement, ev: IonPickerColumnCustomEvent<HTMLIonPickerColumnElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
@@ -4060,39 +4067,52 @@ declare global {
|
||||
prototype: HTMLIonPickerColumnElement;
|
||||
new (): HTMLIonPickerColumnElement;
|
||||
};
|
||||
interface HTMLIonPickerColumnInternalElementEventMap {
|
||||
"ionChange": PickerColumnItem;
|
||||
interface HTMLIonPickerColumnOptionElement extends Components.IonPickerColumnOption, HTMLStencilElement {
|
||||
}
|
||||
interface HTMLIonPickerColumnInternalElement extends Components.IonPickerColumnInternal, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLIonPickerColumnInternalElementEventMap>(type: K, listener: (this: HTMLIonPickerColumnInternalElement, ev: IonPickerColumnInternalCustomEvent<HTMLIonPickerColumnInternalElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLIonPickerColumnInternalElementEventMap>(type: K, listener: (this: HTMLIonPickerColumnInternalElement, ev: IonPickerColumnInternalCustomEvent<HTMLIonPickerColumnInternalElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLIonPickerColumnInternalElement: {
|
||||
prototype: HTMLIonPickerColumnInternalElement;
|
||||
new (): HTMLIonPickerColumnInternalElement;
|
||||
var HTMLIonPickerColumnOptionElement: {
|
||||
prototype: HTMLIonPickerColumnOptionElement;
|
||||
new (): HTMLIonPickerColumnOptionElement;
|
||||
};
|
||||
interface HTMLIonPickerInternalElementEventMap {
|
||||
"ionInputModeChange": PickerInternalChangeEventDetail;
|
||||
interface HTMLIonPickerLegacyElementEventMap {
|
||||
"ionPickerDidPresent": void;
|
||||
"ionPickerWillPresent": void;
|
||||
"ionPickerWillDismiss": OverlayEventDetail;
|
||||
"ionPickerDidDismiss": OverlayEventDetail;
|
||||
"didPresent": void;
|
||||
"willPresent": void;
|
||||
"willDismiss": OverlayEventDetail;
|
||||
"didDismiss": OverlayEventDetail;
|
||||
}
|
||||
interface HTMLIonPickerInternalElement extends Components.IonPickerInternal, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLIonPickerInternalElementEventMap>(type: K, listener: (this: HTMLIonPickerInternalElement, ev: IonPickerInternalCustomEvent<HTMLIonPickerInternalElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
interface HTMLIonPickerLegacyElement extends Components.IonPickerLegacy, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLIonPickerLegacyElementEventMap>(type: K, listener: (this: HTMLIonPickerLegacyElement, ev: IonPickerLegacyCustomEvent<HTMLIonPickerLegacyElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLIonPickerInternalElementEventMap>(type: K, listener: (this: HTMLIonPickerInternalElement, ev: IonPickerInternalCustomEvent<HTMLIonPickerInternalElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLIonPickerLegacyElementEventMap>(type: K, listener: (this: HTMLIonPickerLegacyElement, ev: IonPickerLegacyCustomEvent<HTMLIonPickerLegacyElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLIonPickerInternalElement: {
|
||||
prototype: HTMLIonPickerInternalElement;
|
||||
new (): HTMLIonPickerInternalElement;
|
||||
var HTMLIonPickerLegacyElement: {
|
||||
prototype: HTMLIonPickerLegacyElement;
|
||||
new (): HTMLIonPickerLegacyElement;
|
||||
};
|
||||
interface HTMLIonPickerLegacyColumnElementEventMap {
|
||||
"ionPickerColChange": PickerColumn;
|
||||
}
|
||||
interface HTMLIonPickerLegacyColumnElement extends Components.IonPickerLegacyColumn, HTMLStencilElement {
|
||||
addEventListener<K extends keyof HTMLIonPickerLegacyColumnElementEventMap>(type: K, listener: (this: HTMLIonPickerLegacyColumnElement, ev: IonPickerLegacyColumnCustomEvent<HTMLIonPickerLegacyColumnElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLIonPickerLegacyColumnElementEventMap>(type: K, listener: (this: HTMLIonPickerLegacyColumnElement, ev: IonPickerLegacyColumnCustomEvent<HTMLIonPickerLegacyColumnElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
||||
}
|
||||
var HTMLIonPickerLegacyColumnElement: {
|
||||
prototype: HTMLIonPickerLegacyColumnElement;
|
||||
new (): HTMLIonPickerLegacyColumnElement;
|
||||
};
|
||||
interface HTMLIonPopoverElementEventMap {
|
||||
"ionPopoverDidPresent": void;
|
||||
@@ -4647,8 +4667,9 @@ declare global {
|
||||
"ion-note": HTMLIonNoteElement;
|
||||
"ion-picker": HTMLIonPickerElement;
|
||||
"ion-picker-column": HTMLIonPickerColumnElement;
|
||||
"ion-picker-column-internal": HTMLIonPickerColumnInternalElement;
|
||||
"ion-picker-internal": HTMLIonPickerInternalElement;
|
||||
"ion-picker-column-option": HTMLIonPickerColumnOptionElement;
|
||||
"ion-picker-legacy": HTMLIonPickerLegacyElement;
|
||||
"ion-picker-legacy-column": HTMLIonPickerLegacyColumnElement;
|
||||
"ion-popover": HTMLIonPopoverElement;
|
||||
"ion-progress-bar": HTMLIonProgressBarElement;
|
||||
"ion-radio": HTMLIonRadioElement;
|
||||
@@ -6580,6 +6601,57 @@ declare namespace LocalJSX {
|
||||
"mode"?: "ios" | "md";
|
||||
}
|
||||
interface IonPicker {
|
||||
/**
|
||||
* The mode determines which platform styles to use.
|
||||
*/
|
||||
"mode"?: "ios" | "md";
|
||||
"onIonInputModeChange"?: (event: IonPickerCustomEvent<PickerChangeEventDetail>) => void;
|
||||
}
|
||||
interface IonPickerColumn {
|
||||
/**
|
||||
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
|
||||
*/
|
||||
"color"?: Color;
|
||||
/**
|
||||
* If `true`, the user cannot interact with the picker.
|
||||
*/
|
||||
"disabled"?: boolean;
|
||||
/**
|
||||
* A list of options to be displayed in the picker
|
||||
*/
|
||||
"items"?: PickerColumnItem[];
|
||||
/**
|
||||
* The mode determines which platform styles to use.
|
||||
*/
|
||||
"mode"?: "ios" | "md";
|
||||
/**
|
||||
* If `true`, tapping the picker will reveal a number input keyboard that lets the user type in values for each picker column. This is useful when working with time pickers.
|
||||
*/
|
||||
"numericInput"?: boolean;
|
||||
/**
|
||||
* Emitted when the value has changed.
|
||||
*/
|
||||
"onIonChange"?: (event: IonPickerColumnCustomEvent<string | number | undefined>) => void;
|
||||
/**
|
||||
* The selected option in the picker.
|
||||
*/
|
||||
"value"?: string | number;
|
||||
}
|
||||
interface IonPickerColumnOption {
|
||||
/**
|
||||
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
|
||||
*/
|
||||
"color"?: Color;
|
||||
/**
|
||||
* If `true`, the user cannot interact with the picker column option.
|
||||
*/
|
||||
"disabled"?: boolean;
|
||||
/**
|
||||
* The text value of the option.
|
||||
*/
|
||||
"value"?: any | null;
|
||||
}
|
||||
interface IonPickerLegacy {
|
||||
/**
|
||||
* If `true`, the picker will animate.
|
||||
*/
|
||||
@@ -6633,35 +6705,35 @@ declare namespace LocalJSX {
|
||||
/**
|
||||
* Emitted after the picker has dismissed. Shorthand for ionPickerDidDismiss.
|
||||
*/
|
||||
"onDidDismiss"?: (event: IonPickerCustomEvent<OverlayEventDetail>) => void;
|
||||
"onDidDismiss"?: (event: IonPickerLegacyCustomEvent<OverlayEventDetail>) => void;
|
||||
/**
|
||||
* Emitted after the picker has presented. Shorthand for ionPickerWillDismiss.
|
||||
*/
|
||||
"onDidPresent"?: (event: IonPickerCustomEvent<void>) => void;
|
||||
"onDidPresent"?: (event: IonPickerLegacyCustomEvent<void>) => void;
|
||||
/**
|
||||
* Emitted after the picker has dismissed.
|
||||
*/
|
||||
"onIonPickerDidDismiss"?: (event: IonPickerCustomEvent<OverlayEventDetail>) => void;
|
||||
"onIonPickerDidDismiss"?: (event: IonPickerLegacyCustomEvent<OverlayEventDetail>) => void;
|
||||
/**
|
||||
* Emitted after the picker has presented.
|
||||
*/
|
||||
"onIonPickerDidPresent"?: (event: IonPickerCustomEvent<void>) => void;
|
||||
"onIonPickerDidPresent"?: (event: IonPickerLegacyCustomEvent<void>) => void;
|
||||
/**
|
||||
* Emitted before the picker has dismissed.
|
||||
*/
|
||||
"onIonPickerWillDismiss"?: (event: IonPickerCustomEvent<OverlayEventDetail>) => void;
|
||||
"onIonPickerWillDismiss"?: (event: IonPickerLegacyCustomEvent<OverlayEventDetail>) => void;
|
||||
/**
|
||||
* Emitted before the picker has presented.
|
||||
*/
|
||||
"onIonPickerWillPresent"?: (event: IonPickerCustomEvent<void>) => void;
|
||||
"onIonPickerWillPresent"?: (event: IonPickerLegacyCustomEvent<void>) => void;
|
||||
/**
|
||||
* Emitted before the picker has dismissed. Shorthand for ionPickerWillDismiss.
|
||||
*/
|
||||
"onWillDismiss"?: (event: IonPickerCustomEvent<OverlayEventDetail>) => void;
|
||||
"onWillDismiss"?: (event: IonPickerLegacyCustomEvent<OverlayEventDetail>) => void;
|
||||
/**
|
||||
* Emitted before the picker has presented. Shorthand for ionPickerWillPresent.
|
||||
*/
|
||||
"onWillPresent"?: (event: IonPickerCustomEvent<void>) => void;
|
||||
"onWillPresent"?: (event: IonPickerLegacyCustomEvent<void>) => void;
|
||||
"overlayIndex": number;
|
||||
/**
|
||||
* If `true`, a backdrop will be displayed behind the picker.
|
||||
@@ -6672,7 +6744,7 @@ declare namespace LocalJSX {
|
||||
*/
|
||||
"trigger"?: string | undefined;
|
||||
}
|
||||
interface IonPickerColumn {
|
||||
interface IonPickerLegacyColumn {
|
||||
/**
|
||||
* Picker column data
|
||||
*/
|
||||
@@ -6680,44 +6752,7 @@ declare namespace LocalJSX {
|
||||
/**
|
||||
* Emitted when the selected value has changed
|
||||
*/
|
||||
"onIonPickerColChange"?: (event: IonPickerColumnCustomEvent<PickerColumn>) => void;
|
||||
}
|
||||
interface IonPickerColumnInternal {
|
||||
/**
|
||||
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
|
||||
*/
|
||||
"color"?: Color;
|
||||
/**
|
||||
* If `true`, the user cannot interact with the picker.
|
||||
*/
|
||||
"disabled"?: boolean;
|
||||
/**
|
||||
* A list of options to be displayed in the picker
|
||||
*/
|
||||
"items"?: PickerColumnItem[];
|
||||
/**
|
||||
* The mode determines which platform styles to use.
|
||||
*/
|
||||
"mode"?: "ios" | "md";
|
||||
/**
|
||||
* If `true`, tapping the picker will reveal a number input keyboard that lets the user type in values for each picker column. This is useful when working with time pickers.
|
||||
*/
|
||||
"numericInput"?: boolean;
|
||||
/**
|
||||
* Emitted when the value has changed.
|
||||
*/
|
||||
"onIonChange"?: (event: IonPickerColumnInternalCustomEvent<PickerColumnItem>) => void;
|
||||
/**
|
||||
* The selected option in the picker.
|
||||
*/
|
||||
"value"?: string | number;
|
||||
}
|
||||
interface IonPickerInternal {
|
||||
/**
|
||||
* The mode determines which platform styles to use.
|
||||
*/
|
||||
"mode"?: "ios" | "md";
|
||||
"onIonInputModeChange"?: (event: IonPickerInternalCustomEvent<PickerInternalChangeEventDetail>) => void;
|
||||
"onIonPickerColChange"?: (event: IonPickerLegacyColumnCustomEvent<PickerColumn>) => void;
|
||||
}
|
||||
interface IonPopover {
|
||||
/**
|
||||
@@ -8086,8 +8121,9 @@ declare namespace LocalJSX {
|
||||
"ion-note": IonNote;
|
||||
"ion-picker": IonPicker;
|
||||
"ion-picker-column": IonPickerColumn;
|
||||
"ion-picker-column-internal": IonPickerColumnInternal;
|
||||
"ion-picker-internal": IonPickerInternal;
|
||||
"ion-picker-column-option": IonPickerColumnOption;
|
||||
"ion-picker-legacy": IonPickerLegacy;
|
||||
"ion-picker-legacy-column": IonPickerLegacyColumn;
|
||||
"ion-popover": IonPopover;
|
||||
"ion-progress-bar": IonProgressBar;
|
||||
"ion-radio": IonRadio;
|
||||
@@ -8183,8 +8219,9 @@ declare module "@stencil/core" {
|
||||
"ion-note": LocalJSX.IonNote & JSXBase.HTMLAttributes<HTMLIonNoteElement>;
|
||||
"ion-picker": LocalJSX.IonPicker & JSXBase.HTMLAttributes<HTMLIonPickerElement>;
|
||||
"ion-picker-column": LocalJSX.IonPickerColumn & JSXBase.HTMLAttributes<HTMLIonPickerColumnElement>;
|
||||
"ion-picker-column-internal": LocalJSX.IonPickerColumnInternal & JSXBase.HTMLAttributes<HTMLIonPickerColumnInternalElement>;
|
||||
"ion-picker-internal": LocalJSX.IonPickerInternal & JSXBase.HTMLAttributes<HTMLIonPickerInternalElement>;
|
||||
"ion-picker-column-option": LocalJSX.IonPickerColumnOption & JSXBase.HTMLAttributes<HTMLIonPickerColumnOptionElement>;
|
||||
"ion-picker-legacy": LocalJSX.IonPickerLegacy & JSXBase.HTMLAttributes<HTMLIonPickerLegacyElement>;
|
||||
"ion-picker-legacy-column": LocalJSX.IonPickerLegacyColumn & JSXBase.HTMLAttributes<HTMLIonPickerLegacyColumnElement>;
|
||||
"ion-popover": LocalJSX.IonPopover & JSXBase.HTMLAttributes<HTMLIonPopoverElement>;
|
||||
"ion-progress-bar": LocalJSX.IonProgressBar & JSXBase.HTMLAttributes<HTMLIonProgressBarElement>;
|
||||
"ion-radio": LocalJSX.IonRadio & JSXBase.HTMLAttributes<HTMLIonRadioElement>;
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
}
|
||||
|
||||
async function showPicker() {
|
||||
const picker = Object.assign(document.createElement('ion-picker'), {
|
||||
const picker = Object.assign(document.createElement('ion-picker-legacy'), {
|
||||
columns: [
|
||||
{
|
||||
name: 'Picker',
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
* widest item in the column. Setting a minimum
|
||||
* width avoids this layout shifting.
|
||||
*/
|
||||
ion-picker-column-internal {
|
||||
ion-picker-column {
|
||||
min-width: 26px;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { caretDownSharp, caretUpSharp, chevronBack, chevronDown, chevronForward
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
import type { Color, Mode, StyleEventDetail } from '../../interface';
|
||||
import type { PickerColumnItem } from '../picker-column-internal/picker-column-internal-interfaces';
|
||||
import type { PickerColumnItem } from '../picker-column/picker-column-interfaces';
|
||||
|
||||
import type {
|
||||
DatetimePresentation,
|
||||
@@ -1527,7 +1527,7 @@ export class Datetime implements ComponentInterface {
|
||||
forcePresentation === 'time-date'
|
||||
? [this.renderTimePickerColumns(forcePresentation), this.renderDatePickerColumns(forcePresentation)]
|
||||
: [this.renderDatePickerColumns(forcePresentation), this.renderTimePickerColumns(forcePresentation)];
|
||||
return <ion-picker-internal>{renderArray}</ion-picker-internal>;
|
||||
return <ion-picker>{renderArray}</ion-picker>;
|
||||
}
|
||||
|
||||
private renderDatePickerColumns(forcePresentation: string) {
|
||||
@@ -1613,7 +1613,7 @@ export class Datetime implements ComponentInterface {
|
||||
: `${defaultParts.year}-${defaultParts.month}-${defaultParts.day}`;
|
||||
|
||||
return (
|
||||
<ion-picker-column-internal
|
||||
<ion-picker-column
|
||||
class="date-column"
|
||||
color={this.color}
|
||||
disabled={disabled}
|
||||
@@ -1647,7 +1647,7 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
></ion-picker-column-internal>
|
||||
></ion-picker-column>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1733,7 +1733,7 @@ export class Datetime implements ComponentInterface {
|
||||
const activePart = this.getActivePartsWithFallback();
|
||||
|
||||
return (
|
||||
<ion-picker-column-internal
|
||||
<ion-picker-column
|
||||
class="day-column"
|
||||
color={this.color}
|
||||
disabled={disabled}
|
||||
@@ -1764,7 +1764,7 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
></ion-picker-column-internal>
|
||||
></ion-picker-column>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1778,7 +1778,7 @@ export class Datetime implements ComponentInterface {
|
||||
const activePart = this.getActivePartsWithFallback();
|
||||
|
||||
return (
|
||||
<ion-picker-column-internal
|
||||
<ion-picker-column
|
||||
class="month-column"
|
||||
color={this.color}
|
||||
disabled={disabled}
|
||||
@@ -1809,7 +1809,7 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
></ion-picker-column-internal>
|
||||
></ion-picker-column>
|
||||
);
|
||||
}
|
||||
private renderYearPickerColumn(years: PickerColumnItem[]) {
|
||||
@@ -1822,7 +1822,7 @@ export class Datetime implements ComponentInterface {
|
||||
const activePart = this.getActivePartsWithFallback();
|
||||
|
||||
return (
|
||||
<ion-picker-column-internal
|
||||
<ion-picker-column
|
||||
class="year-column"
|
||||
color={this.color}
|
||||
disabled={disabled}
|
||||
@@ -1853,7 +1853,7 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
></ion-picker-column-internal>
|
||||
></ion-picker-column>
|
||||
);
|
||||
}
|
||||
private renderTimePickerColumns(forcePresentation: string) {
|
||||
@@ -1897,7 +1897,7 @@ export class Datetime implements ComponentInterface {
|
||||
const activePart = this.getActivePartsWithFallback();
|
||||
|
||||
return (
|
||||
<ion-picker-column-internal
|
||||
<ion-picker-column
|
||||
color={this.color}
|
||||
disabled={disabled}
|
||||
value={activePart.hour}
|
||||
@@ -1916,7 +1916,7 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
></ion-picker-column-internal>
|
||||
></ion-picker-column>
|
||||
);
|
||||
}
|
||||
private renderMinutePickerColumn(minutesData: PickerColumnItem[]) {
|
||||
@@ -1926,7 +1926,7 @@ export class Datetime implements ComponentInterface {
|
||||
const activePart = this.getActivePartsWithFallback();
|
||||
|
||||
return (
|
||||
<ion-picker-column-internal
|
||||
<ion-picker-column
|
||||
color={this.color}
|
||||
disabled={disabled}
|
||||
value={activePart.minute}
|
||||
@@ -1945,7 +1945,7 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
></ion-picker-column-internal>
|
||||
></ion-picker-column>
|
||||
);
|
||||
}
|
||||
private renderDayPeriodPickerColumn(dayPeriodData: PickerColumnItem[]) {
|
||||
@@ -1958,7 +1958,7 @@ export class Datetime implements ComponentInterface {
|
||||
const isDayPeriodRTL = isLocaleDayPeriodRTL(this.locale);
|
||||
|
||||
return (
|
||||
<ion-picker-column-internal
|
||||
<ion-picker-column
|
||||
style={isDayPeriodRTL ? { order: '-1' } : {}}
|
||||
color={this.color}
|
||||
disabled={disabled}
|
||||
@@ -1981,7 +1981,7 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
></ion-picker-column-internal>
|
||||
></ion-picker-column>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2345,7 +2345,7 @@ export class Datetime implements ComponentInterface {
|
||||
* This will correctly scroll the element position to the correct time value,
|
||||
* before the popover is fully presented.
|
||||
*/
|
||||
const cols = (ev.target! as HTMLElement).querySelectorAll('ion-picker-column-internal');
|
||||
const cols = (ev.target! as HTMLElement).querySelectorAll('ion-picker-column');
|
||||
// TODO (FW-615): Potentially remove this when intersection observers are fixed in picker column
|
||||
cols.forEach((col) => col.scrollActiveItemIntoView());
|
||||
}}
|
||||
|
||||
@@ -52,28 +52,28 @@
|
||||
}
|
||||
|
||||
/*
|
||||
The second selectors that target ion-picker(-column)-internal
|
||||
The second selectors that target ion-picker(-column)
|
||||
directly are for styling the time picker. This is currently
|
||||
undocumented usage.
|
||||
*/
|
||||
|
||||
.custom-grid-wheel,
|
||||
ion-picker-internal {
|
||||
ion-picker {
|
||||
--wheel-highlight-background: rgb(218, 216, 255);
|
||||
--wheel-fade-background-rgb: 245, 235, 247;
|
||||
}
|
||||
|
||||
ion-picker-internal {
|
||||
ion-picker {
|
||||
background-color: rgb(245, 235, 247);
|
||||
}
|
||||
|
||||
.custom-grid-wheel::part(wheel-item),
|
||||
ion-picker-column-internal::part(wheel-item) {
|
||||
ion-picker-column::part(wheel-item) {
|
||||
color: rgb(255, 134, 154);
|
||||
}
|
||||
|
||||
.custom-grid-wheel::part(wheel-item active),
|
||||
ion-picker-column-internal::part(wheel-item active) {
|
||||
ion-picker-column::part(wheel-item active) {
|
||||
color: rgb(128, 30, 171);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import { h } from '@stencil/core';
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
|
||||
import { Datetime } from '../../../datetime/datetime';
|
||||
import { PickerColumnInternal } from '../../../picker-column-internal/picker-column-internal';
|
||||
import { PickerInternal } from '../../../picker-internal/picker-internal';
|
||||
import { PickerColumn } from '../../../picker-column/picker-column';
|
||||
import { Picker } from '../../../picker/picker';
|
||||
|
||||
describe('ion-datetime disabled', () => {
|
||||
beforeEach(() => {
|
||||
@@ -19,7 +19,7 @@ describe('ion-datetime disabled', () => {
|
||||
|
||||
it('picker should be disabled in prefer wheel mode', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Datetime, PickerColumnInternal, PickerInternal],
|
||||
components: [Datetime, PickerColumn, Picker],
|
||||
template: () => (
|
||||
<ion-datetime id="inline-datetime-wheel" disabled prefer-wheel value="2022-04-21T00:00:00"></ion-datetime>
|
||||
),
|
||||
@@ -28,7 +28,7 @@ describe('ion-datetime disabled', () => {
|
||||
await page.waitForChanges();
|
||||
|
||||
const datetime = page.body.querySelector('ion-datetime')!;
|
||||
const columns = datetime.shadowRoot!.querySelectorAll('ion-picker-column-internal');
|
||||
const columns = datetime.shadowRoot!.querySelectorAll('ion-picker-column');
|
||||
|
||||
await expect(columns.length).toEqual(4);
|
||||
|
||||
|
||||
@@ -109,12 +109,8 @@ configs({ directions: ['ltr'], modes: ['ios'] }).forEach(({ title, config }) =>
|
||||
await page.click('.time-body');
|
||||
await ionPopoverDidPresent.next();
|
||||
|
||||
const hours = page.locator(
|
||||
'ion-popover ion-picker-column-internal:nth-child(1) .picker-item:not(.picker-item-empty)'
|
||||
);
|
||||
const minutes = page.locator(
|
||||
'ion-popover ion-picker-column-internal:nth-child(2) .picker-item:not(.picker-item-empty)'
|
||||
);
|
||||
const hours = page.locator('ion-popover ion-picker-column:nth-child(1) .picker-item:not(.picker-item-empty)');
|
||||
const minutes = page.locator('ion-popover ion-picker-column:nth-child(2) .picker-item:not(.picker-item-empty)');
|
||||
|
||||
expect(await hours.count()).toBe(12);
|
||||
expect(await minutes.count()).toBe(60);
|
||||
@@ -219,7 +215,7 @@ configs({ directions: ['ltr'], modes: ['ios'] }).forEach(({ title, config }) =>
|
||||
);
|
||||
|
||||
const hourPickerItems = page.locator(
|
||||
'ion-datetime ion-picker-column-internal:first-of-type .picker-item:not(.picker-item-empty)'
|
||||
'ion-datetime ion-picker-column:first-of-type .picker-item:not(.picker-item-empty)'
|
||||
);
|
||||
await expect(hourPickerItems).toHaveText(['8', '9', '10', '11']);
|
||||
});
|
||||
@@ -243,7 +239,7 @@ configs({ directions: ['ltr'], modes: ['ios'] }).forEach(({ title, config }) =>
|
||||
);
|
||||
|
||||
const hourPickerItems = page.locator(
|
||||
'ion-datetime ion-picker-column-internal:first-of-type .picker-item:not(.picker-item-empty)'
|
||||
'ion-datetime ion-picker-column:first-of-type .picker-item:not(.picker-item-empty)'
|
||||
);
|
||||
await expect(hourPickerItems).toHaveText(['12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']);
|
||||
});
|
||||
@@ -360,9 +356,7 @@ configs({ directions: ['ltr'], modes: ['ios'] }).forEach(({ title, config }) =>
|
||||
|
||||
await ionPopoverDidPresent.next();
|
||||
|
||||
const hours = page.locator(
|
||||
'ion-popover ion-picker-column-internal:nth-child(1) .picker-item:not(.picker-item-empty)'
|
||||
);
|
||||
const hours = page.locator('ion-popover ion-picker-column:nth-child(1) .picker-item:not(.picker-item-empty)');
|
||||
|
||||
await expect(await hours.count()).toBe(4);
|
||||
});
|
||||
|
||||
@@ -308,7 +308,7 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const columns = page.locator('ion-picker-column-internal');
|
||||
const columns = page.locator('ion-picker-column');
|
||||
|
||||
await expect(columns.nth(0)).toHaveClass(/month-column/);
|
||||
await expect(columns.nth(1)).toHaveClass(/day-column/);
|
||||
@@ -329,7 +329,7 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const columns = page.locator('ion-picker-column-internal');
|
||||
const columns = page.locator('ion-picker-column');
|
||||
|
||||
await expect(columns.nth(0)).toHaveClass(/day-column/);
|
||||
await expect(columns.nth(1)).toHaveClass(/month-column/);
|
||||
|
||||
@@ -227,7 +227,7 @@ class TimePickerFixture {
|
||||
}
|
||||
|
||||
async expectTime(hour: number, minute: number, ampm: string) {
|
||||
const pickerColumns = this.timePicker.locator('ion-picker-column-internal');
|
||||
const pickerColumns = this.timePicker.locator('ion-picker-column');
|
||||
|
||||
await expect(pickerColumns.nth(0)).toHaveJSProperty('value', hour);
|
||||
await expect(pickerColumns.nth(1)).toHaveJSProperty('value', minute);
|
||||
|
||||
@@ -51,7 +51,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
config
|
||||
);
|
||||
|
||||
const items = page.locator('ion-picker-column-internal:first-of-type .picker-item:not(.picker-item-empty)');
|
||||
const items = page.locator('ion-picker-column:first-of-type .picker-item:not(.picker-item-empty)');
|
||||
await expect(items).toHaveText(['1', '2', '3']);
|
||||
});
|
||||
test('should render correct minutes', async ({ page }) => {
|
||||
@@ -62,7 +62,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
config
|
||||
);
|
||||
|
||||
const items = page.locator('ion-picker-column-internal:nth-of-type(2) .picker-item:not(.picker-item-empty)');
|
||||
const items = page.locator('ion-picker-column:nth-of-type(2) .picker-item:not(.picker-item-empty)');
|
||||
await expect(items).toHaveText(['01', '02', '03']);
|
||||
});
|
||||
test('should adjust default parts for allowed hour and minute values', async ({ page }) => {
|
||||
@@ -93,13 +93,11 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const minuteItems = page.locator(
|
||||
'ion-picker-column-internal:nth-of-type(2) .picker-item:not(.picker-item-empty)'
|
||||
);
|
||||
const minuteItems = page.locator('ion-picker-column:nth-of-type(2) .picker-item:not(.picker-item-empty)');
|
||||
await expect(minuteItems).toHaveText(['00', '15', '30', '45']);
|
||||
await expect(minuteItems.nth(1)).toHaveClass(/picker-item-active/);
|
||||
|
||||
const hourItems = page.locator('ion-picker-column-internal:nth-of-type(1) .picker-item:not(.picker-item-empty)');
|
||||
const hourItems = page.locator('ion-picker-column:nth-of-type(1) .picker-item:not(.picker-item-empty)');
|
||||
await expect(hourItems).toHaveText(['2']);
|
||||
await expect(hourItems.nth(0)).toHaveClass(/picker-item-active/);
|
||||
|
||||
@@ -107,7 +105,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
* Since the allowed hour is 2AM, the time period
|
||||
* should switch from PM to AM.
|
||||
*/
|
||||
const ampmItems = page.locator('ion-picker-column-internal:nth-of-type(3) .picker-item:not(.picker-item-empty)');
|
||||
const ampmItems = page.locator('ion-picker-column:nth-of-type(3) .picker-item:not(.picker-item-empty)');
|
||||
await expect(ampmItems).toHaveText(['AM', 'PM']);
|
||||
await expect(ampmItems.nth(0)).toHaveClass(/picker-item-active/);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Mode } from '../../../interface';
|
||||
import type { PickerColumnItem } from '../../picker-column-internal/picker-column-internal-interfaces';
|
||||
import type { PickerColumnItem } from '../../picker-column/picker-column-interfaces';
|
||||
import type { DatetimeParts, DatetimeHourCycle } from '../datetime-interface';
|
||||
|
||||
import { isAfter, isBefore, isSameDay } from './comparison';
|
||||
@@ -380,7 +380,7 @@ export const getMonthColumnData = (
|
||||
* @param minParts The minimum bound on the date that can be returned
|
||||
* @param maxParts The maximum bound on the date that can be returned
|
||||
* @param dayValues The allowed date values
|
||||
* @returns Date data to be used in ion-picker-column-internal
|
||||
* @returns Date data to be used in ion-picker-column
|
||||
*/
|
||||
export const getDayColumnData = (
|
||||
locale: string,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
@import "./picker-column-internal.scss";
|
||||
@@ -1,6 +0,0 @@
|
||||
@import "./picker-column-internal.scss";
|
||||
@import "../../themes/ionic.globals.md";
|
||||
|
||||
:host .picker-item-active {
|
||||
color: current-color(base);
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
@import "../../themes/ionic.globals";
|
||||
|
||||
// Picker Internal
|
||||
// --------------------------------------------------
|
||||
|
||||
:host {
|
||||
@include padding(0px, 16px, 0px, 16px);
|
||||
|
||||
height: 200px;
|
||||
|
||||
outline: none;
|
||||
|
||||
font-size: 22px;
|
||||
scroll-snap-type: y mandatory;
|
||||
|
||||
/**
|
||||
* Need to explicitly set overflow-x: hidden
|
||||
* for older implementations of scroll snapping.
|
||||
*/
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
|
||||
// Hide scrollbars on Firefox
|
||||
scrollbar-width: none;
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide scrollbars on Chrome and Safari
|
||||
*/
|
||||
:host::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host .picker-item {
|
||||
@include padding(0);
|
||||
@include margin(0);
|
||||
|
||||
display: block;
|
||||
|
||||
width: 100%;
|
||||
|
||||
height: 34px;
|
||||
|
||||
border: 0px;
|
||||
|
||||
outline: none;
|
||||
|
||||
background: transparent;
|
||||
|
||||
color: inherit;
|
||||
|
||||
font-family: $font-family-base;
|
||||
|
||||
font-size: inherit;
|
||||
|
||||
line-height: 34px;
|
||||
|
||||
text-align: inherit;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
scroll-snap-align: center;
|
||||
}
|
||||
|
||||
:host .picker-item-empty,
|
||||
:host .picker-item[disabled] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
:host .picker-item-empty,
|
||||
:host(:not([disabled])) .picker-item[disabled] {
|
||||
scroll-snap-align: none;
|
||||
}
|
||||
|
||||
:host([disabled]) {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
:host .picker-item[disabled] {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
:host(.picker-column-active) .picker-item.picker-item-active {
|
||||
color: current-color(base);
|
||||
}
|
||||
|
||||
@media (any-hover: hover) {
|
||||
:host(:focus) {
|
||||
outline: none;
|
||||
|
||||
background: current-color(base, 0.2);
|
||||
}
|
||||
}
|
||||
@@ -1,501 +0,0 @@
|
||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { Component, Element, Event, Host, Method, Prop, State, Watch, h } from '@stencil/core';
|
||||
import { getElementRoot, raf } from '@utils/helpers';
|
||||
import { hapticSelectionChanged, hapticSelectionEnd, hapticSelectionStart } from '@utils/native/haptic';
|
||||
import { isPlatform } from '@utils/platform';
|
||||
import { createColorClasses } from '@utils/theme';
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
import type { Color } from '../../interface';
|
||||
import type { PickerInternalCustomEvent } from '../picker-internal/picker-internal-interfaces';
|
||||
|
||||
import type { PickerColumnItem } from './picker-column-internal-interfaces';
|
||||
|
||||
/**
|
||||
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
|
||||
* @internal
|
||||
*/
|
||||
@Component({
|
||||
tag: 'ion-picker-column-internal',
|
||||
styleUrls: {
|
||||
ios: 'picker-column-internal.ios.scss',
|
||||
md: 'picker-column-internal.md.scss',
|
||||
},
|
||||
shadow: true,
|
||||
})
|
||||
export class PickerColumnInternal implements ComponentInterface {
|
||||
private destroyScrollListener?: () => void;
|
||||
private isScrolling = false;
|
||||
private scrollEndCallback?: () => void;
|
||||
private isColumnVisible = false;
|
||||
private parentEl?: HTMLIonPickerInternalElement | null;
|
||||
private canExitInputMode = true;
|
||||
|
||||
@State() isActive = false;
|
||||
|
||||
@Element() el!: HTMLIonPickerColumnInternalElement;
|
||||
|
||||
/**
|
||||
* If `true`, the user cannot interact with the picker.
|
||||
*/
|
||||
@Prop() disabled = false;
|
||||
|
||||
/**
|
||||
* A list of options to be displayed in the picker
|
||||
*/
|
||||
@Prop() items: PickerColumnItem[] = [];
|
||||
|
||||
/**
|
||||
* The selected option in the picker.
|
||||
*/
|
||||
@Prop({ mutable: true }) value?: string | number;
|
||||
|
||||
/**
|
||||
* The color to use from your application's color palette.
|
||||
* Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`.
|
||||
* For more information on colors, see [theming](/docs/theming/basics).
|
||||
*/
|
||||
@Prop({ reflect: true }) color?: Color = 'primary';
|
||||
|
||||
/**
|
||||
* If `true`, tapping the picker will
|
||||
* reveal a number input keyboard that lets
|
||||
* the user type in values for each picker
|
||||
* column. This is useful when working
|
||||
* with time pickers.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
@Prop() numericInput = false;
|
||||
|
||||
/**
|
||||
* Emitted when the value has changed.
|
||||
*/
|
||||
@Event() ionChange!: EventEmitter<PickerColumnItem>;
|
||||
|
||||
@Watch('value')
|
||||
valueChange() {
|
||||
if (this.isColumnVisible) {
|
||||
/**
|
||||
* Only scroll the active item into view when the picker column
|
||||
* is actively visible to the user.
|
||||
*/
|
||||
this.scrollActiveItemIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only setup scroll listeners
|
||||
* when the picker is visible, otherwise
|
||||
* the container will have a scroll
|
||||
* height of 0px.
|
||||
*/
|
||||
componentWillLoad() {
|
||||
const visibleCallback = (entries: IntersectionObserverEntry[]) => {
|
||||
const ev = entries[0];
|
||||
|
||||
if (ev.isIntersecting) {
|
||||
const { activeItem, el } = this;
|
||||
|
||||
this.isColumnVisible = true;
|
||||
/**
|
||||
* Because this initial call to scrollActiveItemIntoView has to fire before
|
||||
* the scroll listener is set up, we need to manage the active class manually.
|
||||
*/
|
||||
const oldActive = getElementRoot(el).querySelector(`.${PICKER_ITEM_ACTIVE_CLASS}`);
|
||||
if (oldActive) {
|
||||
this.setPickerItemActiveState(oldActive, false);
|
||||
}
|
||||
this.scrollActiveItemIntoView();
|
||||
if (activeItem) {
|
||||
this.setPickerItemActiveState(activeItem, true);
|
||||
}
|
||||
|
||||
this.initializeScrollListener();
|
||||
} else {
|
||||
this.isColumnVisible = false;
|
||||
|
||||
if (this.destroyScrollListener) {
|
||||
this.destroyScrollListener();
|
||||
this.destroyScrollListener = undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
new IntersectionObserver(visibleCallback, { threshold: 0.001 }).observe(this.el);
|
||||
|
||||
const parentEl = (this.parentEl = this.el.closest('ion-picker-internal') as HTMLIonPickerInternalElement | null);
|
||||
if (parentEl !== null) {
|
||||
// TODO(FW-2832): type
|
||||
parentEl.addEventListener('ionInputModeChange', (ev: any) => this.inputModeChange(ev));
|
||||
}
|
||||
}
|
||||
|
||||
componentDidRender() {
|
||||
const { activeItem, items, isColumnVisible, value } = this;
|
||||
|
||||
if (isColumnVisible) {
|
||||
if (activeItem) {
|
||||
this.scrollActiveItemIntoView();
|
||||
} else if (items[0]?.value !== value) {
|
||||
/**
|
||||
* If the picker column does not have an active item and the current value
|
||||
* does not match the first item in the picker column, that means
|
||||
* the value is out of bounds. In this case, we assign the value to the
|
||||
* first item to match the scroll position of the column.
|
||||
*
|
||||
*/
|
||||
this.setValue(items[0].value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@Method()
|
||||
async scrollActiveItemIntoView() {
|
||||
const activeEl = this.activeItem;
|
||||
|
||||
if (activeEl) {
|
||||
this.centerPickerItemInView(activeEl, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value prop and fires the ionChange event.
|
||||
* This is used when we need to fire ionChange from
|
||||
* user-generated events that cannot be caught with normal
|
||||
* input/change event listeners.
|
||||
* @internal
|
||||
*/
|
||||
@Method()
|
||||
async setValue(value?: string | number) {
|
||||
const { items } = this;
|
||||
this.value = value;
|
||||
const findItem = items.find((item) => item.value === value && item.disabled !== true);
|
||||
if (findItem) {
|
||||
this.ionChange.emit(findItem);
|
||||
}
|
||||
}
|
||||
|
||||
private centerPickerItemInView = (target: HTMLElement, smooth = true, canExitInputMode = true) => {
|
||||
const { el, isColumnVisible } = this;
|
||||
if (isColumnVisible) {
|
||||
// (Vertical offset from parent) - (three empty picker rows) + (half the height of the target to ensure the scroll triggers)
|
||||
const top = target.offsetTop - 3 * target.clientHeight + target.clientHeight / 2;
|
||||
|
||||
if (el.scrollTop !== top) {
|
||||
/**
|
||||
* Setting this flag prevents input
|
||||
* mode from exiting in the picker column's
|
||||
* scroll callback. This is useful when the user manually
|
||||
* taps an item or types on the keyboard as both
|
||||
* of these can cause a scroll to occur.
|
||||
*/
|
||||
this.canExitInputMode = canExitInputMode;
|
||||
el.scroll({
|
||||
top,
|
||||
left: 0,
|
||||
behavior: smooth ? 'smooth' : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private setPickerItemActiveState = (item: Element, isActive: boolean) => {
|
||||
if (isActive) {
|
||||
item.classList.add(PICKER_ITEM_ACTIVE_CLASS);
|
||||
item.part.add(PICKER_ITEM_ACTIVE_PART);
|
||||
} else {
|
||||
item.classList.remove(PICKER_ITEM_ACTIVE_CLASS);
|
||||
item.part.remove(PICKER_ITEM_ACTIVE_PART);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* When ionInputModeChange is emitted, each column
|
||||
* needs to check if it is the one being made available
|
||||
* for text entry.
|
||||
*/
|
||||
private inputModeChange = (ev: PickerInternalCustomEvent) => {
|
||||
if (!this.numericInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { useInputMode, inputModeColumn } = ev.detail;
|
||||
|
||||
/**
|
||||
* If inputModeColumn is undefined then this means
|
||||
* all numericInput columns are being selected.
|
||||
*/
|
||||
const isColumnActive = inputModeColumn === undefined || inputModeColumn === this.el;
|
||||
|
||||
if (!useInputMode || !isColumnActive) {
|
||||
this.setInputModeActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setInputModeActive(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Setting isActive will cause a re-render.
|
||||
* As a result, we do not want to cause the
|
||||
* re-render mid scroll as this will cause
|
||||
* the picker column to jump back to
|
||||
* whatever value was selected at the
|
||||
* start of the scroll interaction.
|
||||
*/
|
||||
private setInputModeActive = (state: boolean) => {
|
||||
if (this.isScrolling) {
|
||||
this.scrollEndCallback = () => {
|
||||
this.isActive = state;
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
this.isActive = state;
|
||||
};
|
||||
|
||||
/**
|
||||
* When the column scrolls, the component
|
||||
* needs to determine which item is centered
|
||||
* in the view and will emit an ionChange with
|
||||
* the item object.
|
||||
*/
|
||||
private initializeScrollListener = () => {
|
||||
/**
|
||||
* The haptics for the wheel picker are
|
||||
* an iOS-only feature. As a result, they should
|
||||
* be disabled on Android.
|
||||
*/
|
||||
const enableHaptics = isPlatform('ios');
|
||||
const { el } = this;
|
||||
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined;
|
||||
let activeEl: HTMLElement | null = this.activeItem;
|
||||
|
||||
const scrollCallback = () => {
|
||||
raf(() => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = undefined;
|
||||
}
|
||||
|
||||
if (!this.isScrolling) {
|
||||
enableHaptics && hapticSelectionStart();
|
||||
this.isScrolling = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select item in the center of the column
|
||||
* which is the month/year that we want to select
|
||||
*/
|
||||
const bbox = el.getBoundingClientRect();
|
||||
const centerX = bbox.x + bbox.width / 2;
|
||||
const centerY = bbox.y + bbox.height / 2;
|
||||
|
||||
const activeElement = el.shadowRoot!.elementFromPoint(centerX, centerY) as HTMLButtonElement | null;
|
||||
|
||||
if (activeEl !== null) {
|
||||
this.setPickerItemActiveState(activeEl, false);
|
||||
}
|
||||
|
||||
if (activeElement === null || activeElement.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* If we are selecting a new value,
|
||||
* we need to run haptics again.
|
||||
*/
|
||||
if (activeElement !== activeEl) {
|
||||
enableHaptics && hapticSelectionChanged();
|
||||
|
||||
if (this.canExitInputMode) {
|
||||
/**
|
||||
* The native iOS wheel picker
|
||||
* only dismisses the keyboard
|
||||
* once the selected item has changed
|
||||
* as a result of a swipe
|
||||
* from the user. If `canExitInputMode` is
|
||||
* `false` then this means that the
|
||||
* scroll is happening as a result of
|
||||
* the `value` property programmatically changing
|
||||
* either by an application or by the user via the keyboard.
|
||||
*/
|
||||
this.exitInputMode();
|
||||
}
|
||||
}
|
||||
|
||||
activeEl = activeElement;
|
||||
this.setPickerItemActiveState(activeElement, true);
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
this.isScrolling = false;
|
||||
enableHaptics && hapticSelectionEnd();
|
||||
|
||||
/**
|
||||
* Certain tasks (such as those that
|
||||
* cause re-renders) should only be done
|
||||
* once scrolling has finished, otherwise
|
||||
* flickering may occur.
|
||||
*/
|
||||
const { scrollEndCallback } = this;
|
||||
if (scrollEndCallback) {
|
||||
scrollEndCallback();
|
||||
this.scrollEndCallback = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset this flag as the
|
||||
* next scroll interaction could
|
||||
* be a scroll from the user. In this
|
||||
* case, we should exit input mode.
|
||||
*/
|
||||
this.canExitInputMode = true;
|
||||
|
||||
const dataIndex = activeElement.getAttribute('data-index');
|
||||
|
||||
/**
|
||||
* If no value it is
|
||||
* possible we hit one of the
|
||||
* empty padding columns.
|
||||
*/
|
||||
if (dataIndex === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = parseInt(dataIndex, 10);
|
||||
const selectedItem = this.items[index];
|
||||
|
||||
if (selectedItem.value !== this.value) {
|
||||
this.setValue(selectedItem.value);
|
||||
}
|
||||
}, 250);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrap this in an raf so that the scroll callback
|
||||
* does not fire when component is initially shown.
|
||||
*/
|
||||
raf(() => {
|
||||
el.addEventListener('scroll', scrollCallback);
|
||||
|
||||
this.destroyScrollListener = () => {
|
||||
el.removeEventListener('scroll', scrollCallback);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Tells the parent picker to
|
||||
* exit text entry mode. This is only called
|
||||
* when the selected item changes during scroll, so
|
||||
* we know that the user likely wants to scroll
|
||||
* instead of type.
|
||||
*/
|
||||
private exitInputMode = () => {
|
||||
const { parentEl } = this;
|
||||
|
||||
if (parentEl == null) return;
|
||||
|
||||
parentEl.exitInputMode();
|
||||
|
||||
/**
|
||||
* setInputModeActive only takes
|
||||
* effect once scrolling stops to avoid
|
||||
* a component re-render while scrolling.
|
||||
* However, we want the visual active
|
||||
* indicator to go away immediately, so
|
||||
* we call classList.remove here.
|
||||
*/
|
||||
this.el.classList.remove('picker-column-active');
|
||||
};
|
||||
|
||||
get activeItem() {
|
||||
// If the whole picker column is disabled, the current value should appear active
|
||||
// If the current value item is specifically disabled, it should not appear active
|
||||
const selector = `.picker-item[data-value="${this.value}"]${this.disabled ? '' : ':not([disabled])'}`;
|
||||
|
||||
return getElementRoot(this.el).querySelector(selector) as HTMLElement | null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { items, color, disabled: pickerDisabled, isActive, numericInput } = this;
|
||||
const mode = getIonMode(this);
|
||||
|
||||
/**
|
||||
* exportparts is needed so ion-datetime can expose the parts
|
||||
* from two layers of shadow nesting. If this causes problems,
|
||||
* the attribute can be moved to datetime.tsx and set on every
|
||||
* instance of ion-picker-column-internal there instead.
|
||||
*/
|
||||
|
||||
return (
|
||||
<Host
|
||||
exportparts={`${PICKER_ITEM_PART}, ${PICKER_ITEM_ACTIVE_PART}`}
|
||||
disabled={pickerDisabled}
|
||||
tabindex={pickerDisabled ? null : 0}
|
||||
class={createColorClasses(color, {
|
||||
[mode]: true,
|
||||
['picker-column-active']: isActive,
|
||||
['picker-column-numeric-input']: numericInput,
|
||||
})}
|
||||
>
|
||||
<div class="picker-item picker-item-empty" aria-hidden="true">
|
||||
|
||||
</div>
|
||||
<div class="picker-item picker-item-empty" aria-hidden="true">
|
||||
|
||||
</div>
|
||||
<div class="picker-item picker-item-empty" aria-hidden="true">
|
||||
|
||||
</div>
|
||||
{items.map((item, index) => {
|
||||
const isItemDisabled = pickerDisabled || item.disabled || false;
|
||||
|
||||
{
|
||||
/*
|
||||
Users should be able to tab
|
||||
between multiple columns. As a result,
|
||||
we set tabindex here so that tabbing switches
|
||||
between columns instead of buttons. Users
|
||||
can still use arrow keys on the keyboard to
|
||||
navigate the column up and down.
|
||||
*/
|
||||
}
|
||||
return (
|
||||
<button
|
||||
tabindex="-1"
|
||||
class={{
|
||||
'picker-item': true,
|
||||
}}
|
||||
data-value={item.value}
|
||||
data-index={index}
|
||||
onClick={(ev: Event) => {
|
||||
this.centerPickerItemInView(ev.target as HTMLElement, true);
|
||||
}}
|
||||
disabled={isItemDisabled}
|
||||
part={PICKER_ITEM_PART}
|
||||
>
|
||||
{item.text}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<div class="picker-item picker-item-empty" aria-hidden="true">
|
||||
|
||||
</div>
|
||||
<div class="picker-item picker-item-empty" aria-hidden="true">
|
||||
|
||||
</div>
|
||||
<div class="picker-item picker-item-empty" aria-hidden="true">
|
||||
|
||||
</div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const PICKER_ITEM_ACTIVE_CLASS = 'picker-item-active';
|
||||
const PICKER_ITEM_PART = 'wheel-item';
|
||||
const PICKER_ITEM_ACTIVE_PART = 'active';
|
||||
@@ -0,0 +1 @@
|
||||
@import "./picker-column-option.scss";
|
||||
@@ -0,0 +1,5 @@
|
||||
@import "./picker-column-option.scss";
|
||||
|
||||
:host(.option-active) button {
|
||||
color: current-color(base);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
@import "../../themes/ionic.globals";
|
||||
|
||||
// Picker Column
|
||||
// --------------------------------------------------
|
||||
|
||||
button {
|
||||
@include padding(0);
|
||||
@include margin(0);
|
||||
|
||||
width: 100%;
|
||||
|
||||
height: 34px;
|
||||
|
||||
border: 0px;
|
||||
|
||||
outline: none;
|
||||
|
||||
background: transparent;
|
||||
|
||||
color: inherit;
|
||||
|
||||
font-family: $font-family-base;
|
||||
|
||||
font-size: inherit;
|
||||
|
||||
line-height: 34px;
|
||||
|
||||
text-align: inherit;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:host(.option-disabled) {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
:host(.option-disabled) button {
|
||||
cursor: default;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import type { ComponentInterface } from '@stencil/core';
|
||||
import { Component, Element, Host, Prop, State, Watch, h } from '@stencil/core';
|
||||
import { inheritAttributes } from '@utils/helpers';
|
||||
import { createColorClasses } from '@utils/theme';
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
import type { Color } from '../../interface';
|
||||
|
||||
@Component({
|
||||
tag: 'ion-picker-column-option',
|
||||
styleUrls: {
|
||||
ios: 'picker-column-option.ios.scss',
|
||||
md: 'picker-column-option.md.scss',
|
||||
},
|
||||
shadow: true,
|
||||
})
|
||||
export class PickerColumnOption implements ComponentInterface {
|
||||
@Element() el!: HTMLElement;
|
||||
|
||||
/**
|
||||
* The aria-label of the option.
|
||||
*
|
||||
* If the value changes, then it will trigger a
|
||||
* re-render of the picker since it's a @State variable.
|
||||
* Otherwise, the `aria-label` attribute cannot be updated
|
||||
* after the component is loaded.
|
||||
*/
|
||||
@State() ariaLabel?: string | null = null;
|
||||
|
||||
/**
|
||||
* If `true`, the user cannot interact with the picker column option.
|
||||
*/
|
||||
@Prop() disabled = false;
|
||||
|
||||
/**
|
||||
* The text value of the option.
|
||||
*/
|
||||
@Prop() value?: any | null;
|
||||
|
||||
/**
|
||||
* The color to use from your application's color palette.
|
||||
* Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`.
|
||||
* For more information on colors, see [theming](/docs/theming/basics).
|
||||
*/
|
||||
@Prop({ reflect: true }) color?: Color = 'primary';
|
||||
|
||||
/**
|
||||
* The aria-label of the option has changed after the
|
||||
* first render and needs to be updated within the component.
|
||||
*
|
||||
* @param ariaLbl The new aria-label value.
|
||||
*/
|
||||
@Watch('aria-label')
|
||||
onAriaLabelChange(ariaLbl: string) {
|
||||
this.ariaLabel = ariaLbl;
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
const inheritedAttributes = inheritAttributes(this.el, ['aria-label']);
|
||||
/**
|
||||
* The initial value of `aria-label` needs to be set for
|
||||
* the first render.
|
||||
*/
|
||||
this.ariaLabel = inheritedAttributes['aria-label'] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The column options can load at any time
|
||||
* so the selected option needs to tell the
|
||||
* parent picker column when it is loaded
|
||||
* so the picker column can ensure it is
|
||||
* centered in the view.
|
||||
*/
|
||||
componentDidLoad() {
|
||||
const parentPickerColumn = this.el.closest('ion-picker-column');
|
||||
if (parentPickerColumn !== null && this.value === parentPickerColumn.value) {
|
||||
parentPickerColumn.scrollActiveItemIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When an option is clicked update the
|
||||
* parent picker column value. This
|
||||
* component will handle centering the option
|
||||
* in the column view.
|
||||
*/
|
||||
onClick() {
|
||||
const parentPickerColumn = this.el.closest('ion-picker-column');
|
||||
if (parentPickerColumn !== null) {
|
||||
parentPickerColumn.setValue(this.value);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { color, value, disabled, ariaLabel } = this;
|
||||
const mode = getIonMode(this);
|
||||
|
||||
return (
|
||||
<Host
|
||||
class={createColorClasses(color, {
|
||||
[mode]: true,
|
||||
['option-disabled']: disabled,
|
||||
})}
|
||||
>
|
||||
<button tabindex="-1" aria-label={ariaLabel} disabled={disabled} onClick={() => this.onClick()}>
|
||||
<slot>{value}</slot>
|
||||
</button>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Picker Column Option - a11y</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main>
|
||||
<ion-picker-column-option> my option </ion-picker-column-option>
|
||||
<ion-picker-column-option aria-label="the best one"> other option </ion-picker-column-option>
|
||||
<ion-picker-column-option color="tertiary" class="option-active">option</ion-picker-column-option>
|
||||
<ion-picker-column-option disabled="true">option</ion-picker-column-option>
|
||||
<ion-picker-column-option color="tertiary" class="option-active" disabled="true">option</ion-picker-column-option>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
import AxeBuilder from '@axe-core/playwright';
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This behavior does not vary across directions
|
||||
*/
|
||||
configs({ directions: ['ltr'] }).forEach(({ config, title }) => {
|
||||
test.describe(title('picker column option: a11y'), () => {
|
||||
test('should not have accessibility violations', async ({ page }) => {
|
||||
await page.goto(`/src/components/picker-column-option/test/a11y`, config);
|
||||
|
||||
const results = await new AxeBuilder({ page }).analyze();
|
||||
|
||||
expect(results.violations).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Picker Column Option - Basic</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
<style>
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(250px, 1fr));
|
||||
grid-row-gap: 20px;
|
||||
grid-column-gap: 20px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
|
||||
color: #6f7378;
|
||||
|
||||
margin-top: 10px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header translucent="true">
|
||||
<ion-toolbar>
|
||||
<ion-title>Picker Column Option - Basic</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding">
|
||||
<div class="grid">
|
||||
<div class="grid-item">
|
||||
<h2>Default</h2>
|
||||
<ion-picker-column-option>My Option</ion-picker-column-option>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<h2>Disabled</h2>
|
||||
<ion-picker-column-option disabled="true">My Option</ion-picker-column-option>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<h2>Active</h2>
|
||||
<ion-picker-column-option class="option-active">My Option</ion-picker-column-option>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<h2>Active / Disabled</h2>
|
||||
<ion-picker-column-option class="option-active" disabled="true">My Option</ion-picker-column-option>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,55 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('picker-column-option: rendering'), () => {
|
||||
test('picker option should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-picker-column-option value="option">My Option</ion-picker-column-option>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const option = page.locator('ion-picker-column-option');
|
||||
|
||||
await expect(option).toHaveScreenshot(screenshot('picker-column-option'));
|
||||
});
|
||||
test('disabled picker option should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-picker-column-option disabled="true" value="option">My Option</ion-picker-column-option>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const option = page.locator('ion-picker-column-option');
|
||||
|
||||
await expect(option).toHaveScreenshot(screenshot('disabled-picker-column-option'));
|
||||
});
|
||||
test('active picker option should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-picker-column-option class="option-active" value="option">My Option</ion-picker-column-option>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const option = page.locator('ion-picker-column-option');
|
||||
|
||||
await expect(option).toHaveScreenshot(screenshot('active-picker-column-option'));
|
||||
});
|
||||
test('disabled active picker option should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-picker-column-option class="option-active" disabled="true" value="option">My Option</ion-picker-column-option>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const option = page.locator('ion-picker-column-option');
|
||||
|
||||
await expect(option).toHaveScreenshot(screenshot('disabled-active-picker-column-option'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,31 @@
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
|
||||
import { PickerColumnOption } from '../picker-column-option';
|
||||
|
||||
describe('PickerColumnOption', () => {
|
||||
it('option should be enabled by default', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [PickerColumnOption],
|
||||
html: `
|
||||
<ion-picker-column-option value="a">A</ion-picker-column-option>
|
||||
`,
|
||||
});
|
||||
|
||||
const pickerColumnOption = page.body.querySelector('ion-picker-column-option')!;
|
||||
const button = pickerColumnOption.shadowRoot!.querySelector('button')!;
|
||||
expect(button.hasAttribute('disabled')).toEqual(false);
|
||||
});
|
||||
it('disabled option should have disabled button', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [PickerColumnOption],
|
||||
html: `
|
||||
<ion-picker-column-option value="a" disabled="true">A</ion-picker-column-option>
|
||||
`,
|
||||
});
|
||||
|
||||
const pickerColumnOption = page.body.querySelector('ion-picker-column-option')!;
|
||||
const button = pickerColumnOption.shadowRoot!.querySelector('button')!;
|
||||
|
||||
expect(button.hasAttribute('disabled')).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -1,47 +1 @@
|
||||
@import "./picker-column";
|
||||
@import "../picker/picker.ios.vars";
|
||||
|
||||
// iOS Picker Column
|
||||
// --------------------------------------------------
|
||||
|
||||
.picker-col {
|
||||
@include padding($picker-ios-column-padding-top, $picker-ios-column-padding-end, $picker-ios-column-padding-bottom, $picker-ios-column-padding-start);
|
||||
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.picker-prefix,
|
||||
.picker-suffix,
|
||||
.picker-opts {
|
||||
top: $picker-ios-option-offset-y;
|
||||
|
||||
transform-style: preserve-3d;
|
||||
|
||||
color: inherit;
|
||||
|
||||
font-size: $picker-ios-option-font-size;
|
||||
|
||||
line-height: $picker-ios-option-height;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.picker-opt {
|
||||
@include padding($picker-ios-option-padding-top, $picker-ios-option-padding-end, $picker-ios-option-padding-bottom, $picker-ios-option-padding-start);
|
||||
@include margin(0);
|
||||
@include transform-origin(center, center);
|
||||
|
||||
height: 46px;
|
||||
|
||||
transform-style: preserve-3d;
|
||||
|
||||
transition-timing-function: ease-out;
|
||||
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
|
||||
font-size: $picker-ios-option-font-size;
|
||||
|
||||
line-height: $picker-ios-option-height;
|
||||
backface-visibility: hidden;
|
||||
pointer-events: auto;
|
||||
}
|
||||
@import "./picker-column.scss";
|
||||
|
||||
@@ -1,51 +1,6 @@
|
||||
@import "./picker-column";
|
||||
@import "../picker/picker.md.vars";
|
||||
@import "./picker-column.scss";
|
||||
@import "../../themes/ionic.globals.md";
|
||||
|
||||
// Material Design Picker Column
|
||||
// --------------------------------------------------
|
||||
|
||||
.picker-col {
|
||||
@include padding($picker-md-column-padding-top, $picker-md-column-padding-end, $picker-md-column-padding-bottom, $picker-md-column-padding-start);
|
||||
|
||||
transform-style: preserve-3d;
|
||||
:host .picker-item-active {
|
||||
color: current-color(base);
|
||||
}
|
||||
|
||||
.picker-prefix,
|
||||
.picker-suffix,
|
||||
.picker-opts {
|
||||
top: $picker-md-option-offset-y;
|
||||
|
||||
transform-style: preserve-3d;
|
||||
|
||||
color: inherit;
|
||||
|
||||
font-size: $picker-md-option-font-size;
|
||||
|
||||
line-height: $picker-md-option-height;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
.picker-opt {
|
||||
@include margin(0);
|
||||
@include padding($picker-md-option-padding-top, $picker-md-option-padding-end, $picker-md-option-padding-bottom, $picker-md-option-padding-start);
|
||||
|
||||
height: 43px;
|
||||
|
||||
transition-timing-function: ease-out;
|
||||
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
|
||||
font-size: $picker-md-option-font-size;
|
||||
|
||||
line-height: $picker-md-option-height;
|
||||
backface-visibility: hidden;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.picker-prefix,
|
||||
.picker-suffix,
|
||||
.picker-opt.picker-opt-selected {
|
||||
color: $picker-md-option-selected-color;
|
||||
}
|
||||
@@ -3,87 +3,110 @@
|
||||
// Picker Column
|
||||
// --------------------------------------------------
|
||||
|
||||
.picker-col {
|
||||
display: flex;
|
||||
position: relative;
|
||||
:host {
|
||||
@include padding(0px, 16px, 0px, 16px);
|
||||
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
|
||||
height: 100%;
|
||||
box-sizing: content-box;
|
||||
outline: none;
|
||||
|
||||
contain: content;
|
||||
font-size: 22px;
|
||||
scroll-snap-type: y mandatory;
|
||||
|
||||
/**
|
||||
* Need to explicitly set overflow-x: hidden
|
||||
* for older implementations of scroll snapping.
|
||||
*/
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
|
||||
// Hide scrollbars on Firefox
|
||||
scrollbar-width: none;
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.picker-opts {
|
||||
position: relative;
|
||||
|
||||
flex: 1;
|
||||
|
||||
max-width: 100%;
|
||||
/**
|
||||
* Hide scrollbars on Chrome and Safari
|
||||
*/
|
||||
:host::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// contain property is supported by Chrome
|
||||
.picker-opt {
|
||||
@include position(0, null, null, 0);
|
||||
::slotted(ion-picker-column-option) {
|
||||
display: block;
|
||||
|
||||
scroll-snap-align: center;
|
||||
}
|
||||
|
||||
.picker-item-empty,
|
||||
:host(:not([disabled])) ::slotted(ion-picker-column-option.option-disabled) {
|
||||
scroll-snap-align: none;
|
||||
}
|
||||
|
||||
:host .picker-item {
|
||||
@include padding(0);
|
||||
@include margin(0);
|
||||
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
||||
width: 100%;
|
||||
|
||||
border: 0;
|
||||
height: 34px;
|
||||
|
||||
border: 0px;
|
||||
|
||||
outline: none;
|
||||
|
||||
background: transparent;
|
||||
|
||||
color: inherit;
|
||||
|
||||
font-family: $font-family-base;
|
||||
|
||||
font-size: inherit;
|
||||
|
||||
line-height: 34px;
|
||||
|
||||
text-align: inherit;
|
||||
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
contain: strict;
|
||||
cursor: pointer;
|
||||
|
||||
overflow: hidden;
|
||||
will-change: transform;
|
||||
|
||||
scroll-snap-align: center;
|
||||
}
|
||||
|
||||
.picker-opt.picker-opt-disabled {
|
||||
pointer-events: none;
|
||||
:host .picker-item-empty,
|
||||
:host .picker-item[disabled] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.picker-opt-disabled {
|
||||
opacity: 0;
|
||||
:host .picker-item-empty,
|
||||
:host(:not([disabled])) .picker-item[disabled] {
|
||||
scroll-snap-align: none;
|
||||
}
|
||||
|
||||
.picker-opts-left {
|
||||
justify-content: flex-start;
|
||||
:host([disabled]) {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.picker-opts-right {
|
||||
justify-content: flex-end;
|
||||
:host .picker-item[disabled] {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.picker-opt {
|
||||
&:active,
|
||||
&:focus {
|
||||
:host(.picker-column-active) .picker-item.picker-item-active {
|
||||
color: current-color(base);
|
||||
}
|
||||
|
||||
@media (any-hover: hover) {
|
||||
:host(:focus) {
|
||||
outline: none;
|
||||
|
||||
background: current-color(base, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.picker-prefix {
|
||||
position: relative;
|
||||
|
||||
flex: 1;
|
||||
|
||||
text-align: end;
|
||||
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.picker-suffix {
|
||||
position: relative;
|
||||
|
||||
flex: 1;
|
||||
|
||||
text-align: start;
|
||||
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { Component, Element, Event, Host, Prop, Watch, h } from '@stencil/core';
|
||||
import { clamp } from '@utils/helpers';
|
||||
import { Component, Element, Event, Host, Method, Prop, State, Watch, h } from '@stencil/core';
|
||||
import { getElementRoot, raf } from '@utils/helpers';
|
||||
import { hapticSelectionChanged, hapticSelectionEnd, hapticSelectionStart } from '@utils/native/haptic';
|
||||
import { getClassMap } from '@utils/theme';
|
||||
import { isPlatform } from '@utils/platform';
|
||||
import { createColorClasses } from '@utils/theme';
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
import type { Gesture, GestureDetail } from '../../interface';
|
||||
import type { PickerColumn } from '../picker/picker-interface';
|
||||
import type { Color } from '../../interface';
|
||||
import type { PickerCustomEvent } from '../picker/picker-interfaces';
|
||||
|
||||
import type { PickerColumnItem } from './picker-column-interfaces';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
|
||||
*/
|
||||
@Component({
|
||||
tag: 'ion-picker-column',
|
||||
@@ -17,424 +20,456 @@ import type { PickerColumn } from '../picker/picker-interface';
|
||||
ios: 'picker-column.ios.scss',
|
||||
md: 'picker-column.md.scss',
|
||||
},
|
||||
shadow: true,
|
||||
})
|
||||
export class PickerColumnCmp implements ComponentInterface {
|
||||
private bounceFrom!: number;
|
||||
private lastIndex?: number;
|
||||
private minY!: number;
|
||||
private maxY!: number;
|
||||
private optHeight = 0;
|
||||
private rotateFactor = 0;
|
||||
private scaleFactor = 1;
|
||||
private velocity = 0;
|
||||
private y = 0;
|
||||
private optsEl?: HTMLElement;
|
||||
private gesture?: Gesture;
|
||||
private rafId?: ReturnType<typeof requestAnimationFrame>;
|
||||
private tmrId?: ReturnType<typeof setTimeout>;
|
||||
private noAnimate = true;
|
||||
// `colDidChange` is a flag that gets set when the column is changed
|
||||
// dynamically. When this flag is set, the column will refresh
|
||||
// after the component re-renders to incorporate the new column data.
|
||||
// This is necessary because `this.refresh` queries for the option elements,
|
||||
// so it needs to wait for the latest elements to be available in the DOM.
|
||||
// Ex: column is created with 3 options. User updates the column data
|
||||
// to have 5 options. The column will still think it only has 3 options.
|
||||
private colDidChange = false;
|
||||
export class PickerColumn implements ComponentInterface {
|
||||
private destroyScrollListener?: () => void;
|
||||
private isScrolling = false;
|
||||
private scrollEndCallback?: () => void;
|
||||
private isColumnVisible = false;
|
||||
private parentEl?: HTMLIonPickerElement | null;
|
||||
private canExitInputMode = true;
|
||||
|
||||
@Element() el!: HTMLElement;
|
||||
@State() isActive = false;
|
||||
|
||||
@Element() el!: HTMLIonPickerColumnElement;
|
||||
|
||||
/**
|
||||
* Emitted when the selected value has changed
|
||||
* If `true`, the user cannot interact with the picker.
|
||||
*/
|
||||
@Prop() disabled = false;
|
||||
|
||||
/**
|
||||
* A list of options to be displayed in the picker
|
||||
*/
|
||||
@Prop() items: PickerColumnItem[] = [];
|
||||
|
||||
/**
|
||||
* The selected option in the picker.
|
||||
*/
|
||||
@Prop({ mutable: true }) value?: string | number;
|
||||
|
||||
/**
|
||||
* The color to use from your application's color palette.
|
||||
* Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`.
|
||||
* For more information on colors, see [theming](/docs/theming/basics).
|
||||
*/
|
||||
@Prop({ reflect: true }) color?: Color = 'primary';
|
||||
|
||||
/**
|
||||
* If `true`, tapping the picker will
|
||||
* reveal a number input keyboard that lets
|
||||
* the user type in values for each picker
|
||||
* column. This is useful when working
|
||||
* with time pickers.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
@Event() ionPickerColChange!: EventEmitter<PickerColumn>;
|
||||
@Prop() numericInput = false;
|
||||
|
||||
/** Picker column data */
|
||||
@Prop() col!: PickerColumn;
|
||||
@Watch('col')
|
||||
protected colChanged() {
|
||||
this.colDidChange = true;
|
||||
}
|
||||
/**
|
||||
* Emitted when the value has changed.
|
||||
*/
|
||||
@Event() ionChange!: EventEmitter<string | number | undefined>;
|
||||
|
||||
async connectedCallback() {
|
||||
let pickerRotateFactor = 0;
|
||||
let pickerScaleFactor = 0.81;
|
||||
|
||||
const mode = getIonMode(this);
|
||||
|
||||
if (mode === 'ios') {
|
||||
pickerRotateFactor = -0.46;
|
||||
pickerScaleFactor = 1;
|
||||
}
|
||||
|
||||
this.rotateFactor = pickerRotateFactor;
|
||||
this.scaleFactor = pickerScaleFactor;
|
||||
|
||||
this.gesture = (await import('../../utils/gesture')).createGesture({
|
||||
el: this.el,
|
||||
gestureName: 'picker-swipe',
|
||||
gesturePriority: 100,
|
||||
threshold: 0,
|
||||
passive: false,
|
||||
onStart: (ev) => this.onStart(ev),
|
||||
onMove: (ev) => this.onMove(ev),
|
||||
onEnd: (ev) => this.onEnd(ev),
|
||||
});
|
||||
this.gesture.enable();
|
||||
// Options have not been initialized yet
|
||||
// Animation must be disabled through the `noAnimate` flag
|
||||
// Otherwise, the options will render
|
||||
// at the top of the column and transition down
|
||||
this.tmrId = setTimeout(() => {
|
||||
this.noAnimate = false;
|
||||
// After initialization, `refresh()` will be called
|
||||
// At this point, animation will be enabled. The options will
|
||||
// animate as they are being selected.
|
||||
this.refresh(true);
|
||||
}, 250);
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
this.onDomChange();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
// Options may have changed since last update.
|
||||
if (this.colDidChange) {
|
||||
// Animation must be disabled through the `onDomChange` parameter.
|
||||
// Otherwise, the recently added options will render
|
||||
// at the top of the column and transition down
|
||||
this.onDomChange(true, false);
|
||||
this.colDidChange = false;
|
||||
@Watch('value')
|
||||
valueChange() {
|
||||
if (this.isColumnVisible) {
|
||||
/**
|
||||
* Only scroll the active item into view when the picker column
|
||||
* is actively visible to the user.
|
||||
*/
|
||||
this.scrollActiveItemIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this.rafId !== undefined) cancelAnimationFrame(this.rafId);
|
||||
if (this.tmrId) clearTimeout(this.tmrId);
|
||||
if (this.gesture) {
|
||||
this.gesture.destroy();
|
||||
this.gesture = undefined;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Only setup scroll listeners
|
||||
* when the picker is visible, otherwise
|
||||
* the container will have a scroll
|
||||
* height of 0px.
|
||||
*/
|
||||
componentWillLoad() {
|
||||
const visibleCallback = (entries: IntersectionObserverEntry[]) => {
|
||||
const ev = entries[0];
|
||||
|
||||
private emitColChange() {
|
||||
this.ionPickerColChange.emit(this.col);
|
||||
}
|
||||
if (ev.isIntersecting) {
|
||||
const { activeItem, el } = this;
|
||||
|
||||
private setSelected(selectedIndex: number, duration: number) {
|
||||
// if there is a selected index, then figure out it's y position
|
||||
// if there isn't a selected index, then just use the top y position
|
||||
const y = selectedIndex > -1 ? -(selectedIndex * this.optHeight) : 0;
|
||||
|
||||
this.velocity = 0;
|
||||
|
||||
// set what y position we're at
|
||||
if (this.rafId !== undefined) cancelAnimationFrame(this.rafId);
|
||||
this.update(y, duration, true);
|
||||
|
||||
this.emitColChange();
|
||||
}
|
||||
|
||||
private update(y: number, duration: number, saveY: boolean) {
|
||||
if (!this.optsEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure we've got a good round number :)
|
||||
let translateY = 0;
|
||||
let translateZ = 0;
|
||||
const { col, rotateFactor } = this;
|
||||
const prevSelected = col.selectedIndex;
|
||||
const selectedIndex = (col.selectedIndex = this.indexForY(-y));
|
||||
const durationStr = duration === 0 ? '' : duration + 'ms';
|
||||
const scaleStr = `scale(${this.scaleFactor})`;
|
||||
|
||||
const children = this.optsEl.children;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const button = children[i] as HTMLElement;
|
||||
const opt = col.options[i];
|
||||
const optOffset = i * this.optHeight + y;
|
||||
let transform = '';
|
||||
|
||||
if (rotateFactor !== 0) {
|
||||
const rotateX = optOffset * rotateFactor;
|
||||
if (Math.abs(rotateX) <= 90) {
|
||||
translateY = 0;
|
||||
translateZ = 90;
|
||||
transform = `rotateX(${rotateX}deg) `;
|
||||
} else {
|
||||
translateY = -9999;
|
||||
this.isColumnVisible = true;
|
||||
/**
|
||||
* Because this initial call to scrollActiveItemIntoView has to fire before
|
||||
* the scroll listener is set up, we need to manage the active class manually.
|
||||
*/
|
||||
const oldActive = getElementRoot(el).querySelector(
|
||||
`.${PICKER_ITEM_ACTIVE_CLASS}`
|
||||
) as HTMLIonPickerColumnOptionElement | null;
|
||||
if (oldActive) {
|
||||
this.setPickerItemActiveState(oldActive, false);
|
||||
}
|
||||
this.scrollActiveItemIntoView();
|
||||
if (activeItem) {
|
||||
this.setPickerItemActiveState(activeItem, true);
|
||||
}
|
||||
|
||||
this.initializeScrollListener();
|
||||
} else {
|
||||
translateZ = 0;
|
||||
translateY = optOffset;
|
||||
}
|
||||
this.isColumnVisible = false;
|
||||
|
||||
const selected = selectedIndex === i;
|
||||
transform += `translate3d(0px,${translateY}px,${translateZ}px) `;
|
||||
if (this.scaleFactor !== 1 && !selected) {
|
||||
transform += scaleStr;
|
||||
if (this.destroyScrollListener) {
|
||||
this.destroyScrollListener();
|
||||
this.destroyScrollListener = undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
new IntersectionObserver(visibleCallback, { threshold: 0.001 }).observe(this.el);
|
||||
|
||||
// Update transition duration
|
||||
if (this.noAnimate) {
|
||||
opt.duration = 0;
|
||||
button.style.transitionDuration = '';
|
||||
} else if (duration !== opt.duration) {
|
||||
opt.duration = duration;
|
||||
button.style.transitionDuration = durationStr;
|
||||
}
|
||||
const parentEl = (this.parentEl = this.el.closest('ion-picker') as HTMLIonPickerElement | null);
|
||||
if (parentEl !== null) {
|
||||
// TODO(FW-2832): type
|
||||
parentEl.addEventListener('ionInputModeChange', (ev: any) => this.inputModeChange(ev));
|
||||
}
|
||||
}
|
||||
|
||||
// Update transform
|
||||
if (transform !== opt.transform) {
|
||||
opt.transform = transform;
|
||||
}
|
||||
button.style.transform = transform;
|
||||
componentDidRender() {
|
||||
const { el, activeItem, isColumnVisible, value } = this;
|
||||
|
||||
if (isColumnVisible && !activeItem) {
|
||||
const firstOption = el.querySelector('ion-picker-column-option');
|
||||
|
||||
/**
|
||||
* Ensure that the select column
|
||||
* item has the selected class
|
||||
* If the picker column does not have an active item and the current value
|
||||
* does not match the first item in the picker column, that means
|
||||
* the value is out of bounds. In this case, we assign the value to the
|
||||
* first item to match the scroll position of the column.
|
||||
*
|
||||
*/
|
||||
opt.selected = selected;
|
||||
|
||||
if (selected) {
|
||||
button.classList.add(PICKER_OPT_SELECTED);
|
||||
} else {
|
||||
button.classList.remove(PICKER_OPT_SELECTED);
|
||||
if (firstOption !== null && firstOption.value !== value) {
|
||||
this.setValue(firstOption.value);
|
||||
}
|
||||
}
|
||||
this.col.prevSelected = prevSelected;
|
||||
|
||||
if (saveY) {
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
if (this.lastIndex !== selectedIndex) {
|
||||
// have not set a last index yet
|
||||
hapticSelectionChanged();
|
||||
this.lastIndex = selectedIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private decelerate() {
|
||||
if (this.velocity !== 0) {
|
||||
// still decelerating
|
||||
this.velocity *= DECELERATION_FRICTION;
|
||||
/** @internal */
|
||||
@Method()
|
||||
async scrollActiveItemIntoView() {
|
||||
const activeEl = this.activeItem;
|
||||
|
||||
// do not let it go slower than a velocity of 1
|
||||
this.velocity = this.velocity > 0 ? Math.max(this.velocity, 1) : Math.min(this.velocity, -1);
|
||||
|
||||
let y = this.y + this.velocity;
|
||||
|
||||
if (y > this.minY) {
|
||||
// whoops, it's trying to scroll up farther than the options we have!
|
||||
y = this.minY;
|
||||
this.velocity = 0;
|
||||
} else if (y < this.maxY) {
|
||||
// gahh, it's trying to scroll down farther than we can!
|
||||
y = this.maxY;
|
||||
this.velocity = 0;
|
||||
}
|
||||
|
||||
this.update(y, 0, true);
|
||||
const notLockedIn = Math.round(y) % this.optHeight !== 0 || Math.abs(this.velocity) > 1;
|
||||
if (notLockedIn) {
|
||||
// isn't locked in yet, keep decelerating until it is
|
||||
this.rafId = requestAnimationFrame(() => this.decelerate());
|
||||
} else {
|
||||
this.velocity = 0;
|
||||
this.emitColChange();
|
||||
hapticSelectionEnd();
|
||||
}
|
||||
} else if (this.y % this.optHeight !== 0) {
|
||||
// needs to still get locked into a position so options line up
|
||||
const currentPos = Math.abs(this.y % this.optHeight);
|
||||
|
||||
// create a velocity in the direction it needs to scroll
|
||||
this.velocity = currentPos > this.optHeight / 2 ? 1 : -1;
|
||||
|
||||
this.decelerate();
|
||||
if (activeEl) {
|
||||
this.centerPickerItemInView(activeEl, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
private indexForY(y: number) {
|
||||
return Math.min(Math.max(Math.abs(Math.round(y / this.optHeight)), 0), this.col.options.length - 1);
|
||||
/**
|
||||
* Sets the value prop and fires the ionChange event.
|
||||
* This is used when we need to fire ionChange from
|
||||
* user-generated events that cannot be caught with normal
|
||||
* input/change event listeners.
|
||||
* @internal
|
||||
*/
|
||||
@Method()
|
||||
async setValue(value?: string | number) {
|
||||
if (this.value === value) { return; }
|
||||
|
||||
this.value = value;
|
||||
this.ionChange.emit(value);
|
||||
}
|
||||
|
||||
private onStart(detail: GestureDetail) {
|
||||
// We have to prevent default in order to block scrolling under the picker
|
||||
// but we DO NOT have to stop propagation, since we still want
|
||||
// some "click" events to capture
|
||||
if (detail.event.cancelable) {
|
||||
detail.event.preventDefault();
|
||||
}
|
||||
detail.event.stopPropagation();
|
||||
private centerPickerItemInView = (target: HTMLElement, smooth = true, canExitInputMode = true) => {
|
||||
const { el, isColumnVisible } = this;
|
||||
if (isColumnVisible) {
|
||||
// (Vertical offset from parent) - (three empty picker rows) + (half the height of the target to ensure the scroll triggers)
|
||||
const top = target.offsetTop - 3 * target.clientHeight + target.clientHeight / 2;
|
||||
|
||||
hapticSelectionStart();
|
||||
|
||||
// reset everything
|
||||
if (this.rafId !== undefined) cancelAnimationFrame(this.rafId);
|
||||
const options = this.col.options;
|
||||
let minY = options.length - 1;
|
||||
let maxY = 0;
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
if (!options[i].disabled) {
|
||||
minY = Math.min(minY, i);
|
||||
maxY = Math.max(maxY, i);
|
||||
if (el.scrollTop !== top) {
|
||||
/**
|
||||
* Setting this flag prevents input
|
||||
* mode from exiting in the picker column's
|
||||
* scroll callback. This is useful when the user manually
|
||||
* taps an item or types on the keyboard as both
|
||||
* of these can cause a scroll to occur.
|
||||
*/
|
||||
this.canExitInputMode = canExitInputMode;
|
||||
el.scroll({
|
||||
top,
|
||||
left: 0,
|
||||
behavior: smooth ? 'smooth' : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.minY = -(minY * this.optHeight);
|
||||
this.maxY = -(maxY * this.optHeight);
|
||||
}
|
||||
|
||||
private onMove(detail: GestureDetail) {
|
||||
if (detail.event.cancelable) {
|
||||
detail.event.preventDefault();
|
||||
}
|
||||
detail.event.stopPropagation();
|
||||
|
||||
// update the scroll position relative to pointer start position
|
||||
let y = this.y + detail.deltaY;
|
||||
|
||||
if (y > this.minY) {
|
||||
// scrolling up higher than scroll area
|
||||
y = Math.pow(y, 0.8);
|
||||
this.bounceFrom = y;
|
||||
} else if (y < this.maxY) {
|
||||
// scrolling down below scroll area
|
||||
y += Math.pow(this.maxY - y, 0.9);
|
||||
this.bounceFrom = y;
|
||||
private setPickerItemActiveState = (item: HTMLIonPickerColumnOptionElement, isActive: boolean) => {
|
||||
if (isActive) {
|
||||
item.classList.add(PICKER_ITEM_ACTIVE_CLASS);
|
||||
item.part.add(PICKER_ITEM_ACTIVE_PART);
|
||||
} else {
|
||||
this.bounceFrom = 0;
|
||||
item.classList.remove(PICKER_ITEM_ACTIVE_CLASS);
|
||||
item.part.remove(PICKER_ITEM_ACTIVE_PART);
|
||||
}
|
||||
};
|
||||
|
||||
this.update(y, 0, false);
|
||||
}
|
||||
|
||||
private onEnd(detail: GestureDetail) {
|
||||
if (this.bounceFrom > 0) {
|
||||
// bounce back up
|
||||
this.update(this.minY, 100, true);
|
||||
this.emitColChange();
|
||||
return;
|
||||
} else if (this.bounceFrom < 0) {
|
||||
// bounce back down
|
||||
this.update(this.maxY, 100, true);
|
||||
this.emitColChange();
|
||||
/**
|
||||
* When ionInputModeChange is emitted, each column
|
||||
* needs to check if it is the one being made available
|
||||
* for text entry.
|
||||
*/
|
||||
private inputModeChange = (ev: PickerCustomEvent) => {
|
||||
if (!this.numericInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.velocity = clamp(-MAX_PICKER_SPEED, detail.velocityY * 23, MAX_PICKER_SPEED);
|
||||
if (this.velocity === 0 && detail.deltaY === 0) {
|
||||
const opt = (detail.event.target as Element).closest('.picker-opt');
|
||||
if (opt?.hasAttribute('opt-index')) {
|
||||
this.setSelected(parseInt(opt.getAttribute('opt-index')!, 10), TRANSITION_DURATION);
|
||||
}
|
||||
} else {
|
||||
this.y += detail.deltaY;
|
||||
|
||||
if (Math.abs(detail.velocityY) < 0.05) {
|
||||
const isScrollingUp = detail.deltaY > 0;
|
||||
const optHeightFraction = (Math.abs(this.y) % this.optHeight) / this.optHeight;
|
||||
|
||||
if (isScrollingUp && optHeightFraction > 0.5) {
|
||||
this.velocity = Math.abs(this.velocity) * -1;
|
||||
} else if (!isScrollingUp && optHeightFraction <= 0.5) {
|
||||
this.velocity = Math.abs(this.velocity);
|
||||
}
|
||||
}
|
||||
|
||||
this.decelerate();
|
||||
}
|
||||
}
|
||||
|
||||
private refresh(forceRefresh?: boolean, animated?: boolean) {
|
||||
let min = this.col.options.length - 1;
|
||||
let max = 0;
|
||||
const options = this.col.options;
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
if (!options[i].disabled) {
|
||||
min = Math.min(min, i);
|
||||
max = Math.max(max, i);
|
||||
}
|
||||
}
|
||||
const { useInputMode, inputModeColumn } = ev.detail;
|
||||
|
||||
/**
|
||||
* Only update selected value if column has a
|
||||
* velocity of 0. If it does not, then the
|
||||
* column is animating might land on
|
||||
* a value different than the value at
|
||||
* selectedIndex
|
||||
* If inputModeColumn is undefined then this means
|
||||
* all numericInput columns are being selected.
|
||||
*/
|
||||
if (this.velocity !== 0) {
|
||||
const isColumnActive = inputModeColumn === undefined || inputModeColumn === this.el;
|
||||
|
||||
if (!useInputMode || !isColumnActive) {
|
||||
this.setInputModeActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIndex = clamp(min, this.col.selectedIndex ?? 0, max);
|
||||
if (this.col.prevSelected !== selectedIndex || forceRefresh) {
|
||||
const y = selectedIndex * this.optHeight * -1;
|
||||
const duration = animated ? TRANSITION_DURATION : 0;
|
||||
this.velocity = 0;
|
||||
this.update(y, duration, true);
|
||||
}
|
||||
}
|
||||
this.setInputModeActive(true);
|
||||
};
|
||||
|
||||
private onDomChange(forceRefresh?: boolean, animated?: boolean) {
|
||||
const colEl = this.optsEl;
|
||||
if (colEl) {
|
||||
// DOM READ
|
||||
// We perfom a DOM read over a rendered item, this needs to happen after the first render or after the the column has changed
|
||||
this.optHeight = colEl.firstElementChild ? colEl.firstElementChild.clientHeight : 0;
|
||||
/**
|
||||
* Setting isActive will cause a re-render.
|
||||
* As a result, we do not want to cause the
|
||||
* re-render mid scroll as this will cause
|
||||
* the picker column to jump back to
|
||||
* whatever value was selected at the
|
||||
* start of the scroll interaction.
|
||||
*/
|
||||
private setInputModeActive = (state: boolean) => {
|
||||
if (this.isScrolling) {
|
||||
this.scrollEndCallback = () => {
|
||||
this.isActive = state;
|
||||
};
|
||||
return;
|
||||
}
|
||||
this.refresh(forceRefresh, animated);
|
||||
|
||||
this.isActive = state;
|
||||
};
|
||||
|
||||
/**
|
||||
* When the column scrolls, the component
|
||||
* needs to determine which item is centered
|
||||
* in the view and will emit an ionChange with
|
||||
* the item object.
|
||||
*/
|
||||
private initializeScrollListener = () => {
|
||||
/**
|
||||
* The haptics for the wheel picker are
|
||||
* an iOS-only feature. As a result, they should
|
||||
* be disabled on Android.
|
||||
*/
|
||||
const enableHaptics = isPlatform('ios');
|
||||
const { el } = this;
|
||||
|
||||
let timeout: ReturnType<typeof setTimeout> | undefined;
|
||||
let activeEl: HTMLIonPickerColumnOptionElement | undefined = this.activeItem;
|
||||
|
||||
const scrollCallback = () => {
|
||||
raf(() => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = undefined;
|
||||
}
|
||||
|
||||
if (!this.isScrolling) {
|
||||
enableHaptics && hapticSelectionStart();
|
||||
this.isScrolling = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select item in the center of the column
|
||||
* which is the month/year that we want to select
|
||||
*/
|
||||
const bbox = el.getBoundingClientRect();
|
||||
const centerX = bbox.x + bbox.width / 2;
|
||||
const centerY = bbox.y + bbox.height / 2;
|
||||
|
||||
const newActiveElement = el.shadowRoot!.elementFromPoint(
|
||||
centerX,
|
||||
centerY
|
||||
) as HTMLIonPickerColumnOptionElement | null;
|
||||
|
||||
if (activeEl !== undefined) {
|
||||
this.setPickerItemActiveState(activeEl, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* This null check is important because activeEl
|
||||
* can be undefined but newActiveElement can be
|
||||
* null since we are using a querySelector.
|
||||
* newActiveElement !== activeEl would return true
|
||||
* below if newActiveElement was null but activeEl
|
||||
* was undefined.
|
||||
*/
|
||||
if (newActiveElement === null || newActiveElement.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* If we are selecting a new value,
|
||||
* we need to run haptics again.
|
||||
*/
|
||||
if (newActiveElement !== activeEl) {
|
||||
enableHaptics && hapticSelectionChanged();
|
||||
|
||||
if (this.canExitInputMode) {
|
||||
/**
|
||||
* The native iOS wheel picker
|
||||
* only dismisses the keyboard
|
||||
* once the selected item has changed
|
||||
* as a result of a swipe
|
||||
* from the user. If `canExitInputMode` is
|
||||
* `false` then this means that the
|
||||
* scroll is happening as a result of
|
||||
* the `value` property programmatically changing
|
||||
* either by an application or by the user via the keyboard.
|
||||
*/
|
||||
this.exitInputMode();
|
||||
}
|
||||
}
|
||||
|
||||
activeEl = newActiveElement;
|
||||
this.setPickerItemActiveState(newActiveElement, true);
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
this.isScrolling = false;
|
||||
enableHaptics && hapticSelectionEnd();
|
||||
|
||||
/**
|
||||
* Certain tasks (such as those that
|
||||
* cause re-renders) should only be done
|
||||
* once scrolling has finished, otherwise
|
||||
* flickering may occur.
|
||||
*/
|
||||
const { scrollEndCallback } = this;
|
||||
if (scrollEndCallback) {
|
||||
scrollEndCallback();
|
||||
this.scrollEndCallback = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset this flag as the
|
||||
* next scroll interaction could
|
||||
* be a scroll from the user. In this
|
||||
* case, we should exit input mode.
|
||||
*/
|
||||
this.canExitInputMode = true;
|
||||
|
||||
this.setValue(newActiveElement.value);
|
||||
}, 250);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrap this in an raf so that the scroll callback
|
||||
* does not fire when component is initially shown.
|
||||
*/
|
||||
raf(() => {
|
||||
el.addEventListener('scroll', scrollCallback);
|
||||
|
||||
this.destroyScrollListener = () => {
|
||||
el.removeEventListener('scroll', scrollCallback);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Tells the parent picker to
|
||||
* exit text entry mode. This is only called
|
||||
* when the selected item changes during scroll, so
|
||||
* we know that the user likely wants to scroll
|
||||
* instead of type.
|
||||
*/
|
||||
private exitInputMode = () => {
|
||||
const { parentEl } = this;
|
||||
|
||||
if (parentEl == null) return;
|
||||
|
||||
parentEl.exitInputMode();
|
||||
|
||||
/**
|
||||
* setInputModeActive only takes
|
||||
* effect once scrolling stops to avoid
|
||||
* a component re-render while scrolling.
|
||||
* However, we want the visual active
|
||||
* indicator to go away immediately, so
|
||||
* we call classList.remove here.
|
||||
*/
|
||||
this.el.classList.remove('picker-column-active');
|
||||
};
|
||||
|
||||
get activeItem() {
|
||||
const { value } = this;
|
||||
const options = Array.from(
|
||||
this.el.querySelectorAll('ion-picker-column-option') as NodeListOf<HTMLIonPickerColumnOptionElement>
|
||||
);
|
||||
return options.find((option: HTMLIonPickerColumnOptionElement) => {
|
||||
/**
|
||||
* If the whole picker column is disabled, the current value should appear active
|
||||
* If the current value item is specifically disabled, it should not appear active
|
||||
*/
|
||||
if (!this.disabled && option.disabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return option.value === value;
|
||||
}) as HTMLIonPickerColumnOptionElement | undefined;
|
||||
}
|
||||
|
||||
render() {
|
||||
const col = this.col;
|
||||
const { color, disabled: pickerDisabled, isActive, numericInput } = this;
|
||||
const mode = getIonMode(this);
|
||||
|
||||
/**
|
||||
* exportparts is needed so ion-datetime can expose the parts
|
||||
* from two layers of shadow nesting. If this causes problems,
|
||||
* the attribute can be moved to datetime.tsx and set on every
|
||||
* instance of ion-picker-column there instead.
|
||||
*/
|
||||
|
||||
return (
|
||||
<Host
|
||||
class={{
|
||||
exportparts={`${PICKER_ITEM_PART}, ${PICKER_ITEM_ACTIVE_PART}`}
|
||||
disabled={pickerDisabled}
|
||||
tabindex={pickerDisabled ? null : 0}
|
||||
class={createColorClasses(color, {
|
||||
[mode]: true,
|
||||
'picker-col': true,
|
||||
'picker-opts-left': this.col.align === 'left',
|
||||
'picker-opts-right': this.col.align === 'right',
|
||||
...getClassMap(col.cssClass),
|
||||
}}
|
||||
style={{
|
||||
'max-width': this.col.columnWidth,
|
||||
}}
|
||||
['picker-column-active']: isActive,
|
||||
['picker-column-numeric-input']: numericInput,
|
||||
})}
|
||||
>
|
||||
{col.prefix && (
|
||||
<div class="picker-prefix" style={{ width: col.prefixWidth! }}>
|
||||
{col.prefix}
|
||||
</div>
|
||||
)}
|
||||
<div class="picker-opts" style={{ maxWidth: col.optionsWidth! }} ref={(el) => (this.optsEl = el)}>
|
||||
{col.options.map((o, index) => (
|
||||
<button
|
||||
aria-label={o.ariaLabel}
|
||||
class={{ 'picker-opt': true, 'picker-opt-disabled': !!o.disabled }}
|
||||
opt-index={index}
|
||||
>
|
||||
{o.text}
|
||||
</button>
|
||||
))}
|
||||
<div class="picker-item picker-item-empty" aria-hidden="true">
|
||||
|
||||
</div>
|
||||
<div class="picker-item picker-item-empty" aria-hidden="true">
|
||||
|
||||
</div>
|
||||
<div class="picker-item picker-item-empty" aria-hidden="true">
|
||||
|
||||
</div>
|
||||
<slot></slot>
|
||||
<div class="picker-item picker-item-empty" aria-hidden="true">
|
||||
|
||||
</div>
|
||||
<div class="picker-item picker-item-empty" aria-hidden="true">
|
||||
|
||||
</div>
|
||||
<div class="picker-item picker-item-empty" aria-hidden="true">
|
||||
|
||||
</div>
|
||||
{col.suffix && (
|
||||
<div class="picker-suffix" style={{ width: col.suffixWidth! }}>
|
||||
{col.suffix}
|
||||
</div>
|
||||
)}
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const PICKER_OPT_SELECTED = 'picker-opt-selected';
|
||||
const DECELERATION_FRICTION = 0.97;
|
||||
const MAX_PICKER_SPEED = 90;
|
||||
const TRANSITION_DURATION = 150;
|
||||
const PICKER_ITEM_ACTIVE_CLASS = 'option-active';
|
||||
const PICKER_ITEM_PART = 'wheel-item';
|
||||
const PICKER_ITEM_ACTIVE_PART = 'active';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Picker Column Internal - Basic</title>
|
||||
<title>Picker Column - Basic</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
@@ -39,16 +39,16 @@
|
||||
<ion-app>
|
||||
<ion-header translucent="true">
|
||||
<ion-toolbar>
|
||||
<ion-title>Picker Column Internal - Basic</ion-title>
|
||||
<ion-title>Picker Column - Basic</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding">
|
||||
<div class="grid">
|
||||
<div class="grid-item">
|
||||
<h2>Default</h2>
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal id="default"></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
<ion-picker>
|
||||
<ion-picker-column id="default"></ion-picker-column>
|
||||
</ion-picker>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
@@ -57,12 +57,18 @@
|
||||
|
||||
const items = Array(24)
|
||||
.fill()
|
||||
.map((_, i) => ({
|
||||
text: `${i}`,
|
||||
value: i,
|
||||
}));
|
||||
.forEach((_, i) => {
|
||||
const option = document.createElement('ion-picker-column-option');
|
||||
option.value = i;
|
||||
option.textContent = i;
|
||||
|
||||
defaultPickerColumn.items = items;
|
||||
defaultPickerColumn.appendChild(option);
|
||||
});
|
||||
//defaultPickerColumn.value = 11;
|
||||
|
||||
defaultPickerColumn.addEventListener('ionChange', (ev) => {
|
||||
console.log(ev);
|
||||
})
|
||||
</script>
|
||||
</ion-app>
|
||||
</body>
|
||||
@@ -5,33 +5,28 @@ import { configs, test } from '@utils/test/playwright';
|
||||
* This behavior does not vary across modes/directions.
|
||||
*/
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('picker-column-internal'), () => {
|
||||
test.describe(title('picker-column'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/src/components/picker-column-internal/test/basic', config);
|
||||
});
|
||||
|
||||
test('should render a picker item for each item', async ({ page }) => {
|
||||
const columns = page.locator('ion-picker-column-internal .picker-item:not(.picker-item-empty)');
|
||||
await expect(columns).toHaveCount(24);
|
||||
await page.goto('/src/components/picker-column/test/basic', config);
|
||||
});
|
||||
|
||||
test('should render 6 empty picker items', async ({ page }) => {
|
||||
const columns = page.locator('ion-picker-column-internal .picker-item-empty');
|
||||
const columns = page.locator('ion-picker-column .picker-item-empty');
|
||||
await expect(columns).toHaveCount(6);
|
||||
});
|
||||
|
||||
test('should not have an active item when value is not set', async ({ page }) => {
|
||||
const activeColumn = page.locator('ion-picker-column-internal .picker-item-active');
|
||||
const activeColumn = page.locator('ion-picker-column .option-active');
|
||||
await expect(activeColumn).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('should have an active item when value is set', async ({ page }) => {
|
||||
await page.locator('#default').evaluate((el: HTMLIonPickerColumnInternalElement) => {
|
||||
await page.locator('#default').evaluate((el: HTMLIonPickerColumnElement) => {
|
||||
el.value = '12';
|
||||
});
|
||||
await page.waitForChanges();
|
||||
|
||||
const activeColumn = page.locator('ion-picker-column-internal .picker-item-active');
|
||||
const activeColumn = page.locator('ion-picker-column .option-active');
|
||||
|
||||
expect(activeColumn).not.toBeNull();
|
||||
});
|
||||
@@ -40,14 +35,14 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
test.skip('scrolling should change the active item', async ({ page, skip }) => {
|
||||
skip.browser('firefox', 'https://bugzilla.mozilla.org/show_bug.cgi?id=1766890');
|
||||
|
||||
await page.locator('#default').evaluate((el: HTMLIonPickerColumnInternalElement) => {
|
||||
await page.locator('#default').evaluate((el: HTMLIonPickerColumnElement) => {
|
||||
el.scrollTop = 801;
|
||||
});
|
||||
await page.waitForChanges();
|
||||
|
||||
const activeColumn = page.locator('ion-picker-column-internal .picker-item-active');
|
||||
const activeColumn = page.locator('ion-picker-column .option-active');
|
||||
|
||||
expect(await activeColumn?.innerText()).toEqual('23');
|
||||
await expect(activeColumn).toHaveJSProperty('value', 23);
|
||||
});
|
||||
|
||||
test('should not emit ionChange when the value is modified externally', async ({ page, skip }) => {
|
||||
@@ -55,7 +50,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
|
||||
const ionChangeSpy = await page.spyOnEvent('ionChange');
|
||||
|
||||
await page.locator('#default').evaluate((el: HTMLIonPickerColumnInternalElement) => {
|
||||
await page.locator('#default').evaluate((el: HTMLIonPickerColumnElement) => {
|
||||
el.value = '12';
|
||||
});
|
||||
|
||||
@@ -68,7 +63,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
|
||||
const ionChangeSpy = await page.spyOnEvent('ionChange');
|
||||
|
||||
await page.locator('#default').evaluate((el: HTMLIonPickerColumnInternalElement) => {
|
||||
await page.locator('#default').evaluate((el: HTMLIonPickerColumnElement) => {
|
||||
el.scrollTo(0, el.scrollHeight);
|
||||
});
|
||||
await page.waitForChanges();
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Picker Column Internal - Basic</title>
|
||||
<title>Picker Column - Basic</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
@@ -39,45 +39,50 @@
|
||||
<ion-app>
|
||||
<ion-header translucent="true">
|
||||
<ion-toolbar>
|
||||
<ion-title>Picker Column Internal - Disabled</ion-title>
|
||||
<ion-title>Picker Column - Disabled</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding">
|
||||
<div class="grid">
|
||||
<div class="grid-item">
|
||||
<h2>Even items disabled</h2>
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal id="half-disabled"></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
<ion-picker>
|
||||
<ion-picker-column id="half-disabled"></ion-picker-column>
|
||||
</ion-picker>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<h2>Column disabled</h2>
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal id="column-disabled" value="11" disabled></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
<ion-picker>
|
||||
<ion-picker-column id="column-disabled" disabled="true"></ion-picker-column>
|
||||
</ion-picker>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
<script>
|
||||
const halfDisabledPicker = document.getElementById('half-disabled');
|
||||
const halfDisabledItems = Array(24)
|
||||
Array(24)
|
||||
.fill()
|
||||
.map((_, i) => ({
|
||||
text: `${i}`,
|
||||
value: i,
|
||||
disabled: i % 2 === 0,
|
||||
}));
|
||||
halfDisabledPicker.items = halfDisabledItems;
|
||||
halfDisabledPicker.value = 12;
|
||||
.forEach((_, i) => {
|
||||
const option = document.createElement('ion-picker-column-option');
|
||||
option.value = i;
|
||||
option.textContent = i;
|
||||
option.disabled = i % 2 === 0;
|
||||
|
||||
halfDisabledPicker.appendChild(option);
|
||||
});
|
||||
halfDisabledPicker.value = 4;
|
||||
|
||||
const fullDisabledPicker = document.getElementById('column-disabled');
|
||||
const items = Array(24)
|
||||
Array(24)
|
||||
.fill()
|
||||
.map((_, i) => ({
|
||||
text: `${i}`,
|
||||
value: i,
|
||||
}));
|
||||
fullDisabledPicker.items = items;
|
||||
.forEach((_, i) => {
|
||||
const option = document.createElement('ion-picker-column-option');
|
||||
option.value = i;
|
||||
option.textContent = i;
|
||||
|
||||
fullDisabledPicker.appendChild(option);
|
||||
});
|
||||
fullDisabledPicker.value = 11;
|
||||
</script>
|
||||
</ion-app>
|
||||
</body>
|
||||
@@ -1,50 +1,20 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This behavior does not vary across directions.
|
||||
*/
|
||||
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('picker-column-internal: disabled rendering'), () => {
|
||||
test('should not have visual regressions', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal value="b"></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
column.items = [
|
||||
{ text: 'A', value: 'a', disabled: true },
|
||||
{ text: 'B', value: 'b' },
|
||||
{ text: 'C', value: 'c', disabled: true }
|
||||
]
|
||||
</script>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const picker = page.locator('ion-picker-internal');
|
||||
await expect(picker).toHaveScreenshot(screenshot(`picker-internal-disabled`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This behavior does not vary across modes/directions.
|
||||
*/
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('picker-column-internal: disabled items'), () => {
|
||||
test.describe(title('picker-column: disabled items'), () => {
|
||||
test('all picker items should be enabled by default', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
<ion-picker>
|
||||
<ion-picker-column></ion-picker-column>
|
||||
</ion-picker>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
const column = document.querySelector('ion-picker-column');
|
||||
column.items = [
|
||||
{ text: 'A', value: 'a' },
|
||||
{ text: 'B', value: 'b' },
|
||||
@@ -55,19 +25,19 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
config
|
||||
);
|
||||
|
||||
const pickerItems = page.locator('ion-picker-column-internal .picker-item:not(.picker-item-empty, [disabled])');
|
||||
const pickerItems = page.locator('ion-picker-column .picker-item:not(.picker-item-empty, [disabled])');
|
||||
|
||||
expect(await pickerItems.count()).toBe(3);
|
||||
});
|
||||
test('disabled picker item should not be interactive', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
<ion-picker>
|
||||
<ion-picker-column></ion-picker-column>
|
||||
</ion-picker>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
const column = document.querySelector('ion-picker-column');
|
||||
column.items = [
|
||||
{ text: 'A', value: 'a' },
|
||||
{ text: 'B', value: 'b', disabled: true },
|
||||
@@ -78,18 +48,18 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
config
|
||||
);
|
||||
|
||||
const disabledItem = page.locator('ion-picker-column-internal .picker-item[disabled]');
|
||||
const disabledItem = page.locator('ion-picker-column .picker-item[disabled]');
|
||||
await expect(disabledItem).not.toBeEnabled();
|
||||
});
|
||||
test('disabled picker item should not be considered active', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal value="b"></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
<ion-picker>
|
||||
<ion-picker-column value="b"></ion-picker-column>
|
||||
</ion-picker>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
const column = document.querySelector('ion-picker-column');
|
||||
column.items = [
|
||||
{ text: 'A', value: 'a' },
|
||||
{ text: 'B', value: 'b', disabled: true },
|
||||
@@ -100,18 +70,18 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
config
|
||||
);
|
||||
|
||||
const disabledItem = page.locator('ion-picker-column-internal .picker-item[data-value="b"]');
|
||||
const disabledItem = page.locator('ion-picker-column .picker-item[data-value="b"]');
|
||||
await expect(disabledItem).not.toHaveClass(/picker-item-active/);
|
||||
});
|
||||
test('setting the value to a disabled item should not cause that item to be active', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
<ion-picker>
|
||||
<ion-picker-column></ion-picker-column>
|
||||
</ion-picker>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
const column = document.querySelector('ion-picker-column');
|
||||
column.items = [
|
||||
{ text: 'A', value: 'a' },
|
||||
{ text: 'B', value: 'b', disabled: true },
|
||||
@@ -122,24 +92,24 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
config
|
||||
);
|
||||
|
||||
const pickerColumn = page.locator('ion-picker-column-internal');
|
||||
await pickerColumn.evaluate((el: HTMLIonPickerColumnInternalElement) => (el.value = 'b'));
|
||||
const pickerColumn = page.locator('ion-picker-column');
|
||||
await pickerColumn.evaluate((el: HTMLIonPickerColumnElement) => (el.value = 'b'));
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
const disabledItem = page.locator('ion-picker-column-internal .picker-item[data-value="b"]');
|
||||
const disabledItem = page.locator('ion-picker-column .picker-item[data-value="b"]');
|
||||
await expect(disabledItem).toBeDisabled();
|
||||
await expect(disabledItem).not.toHaveClass(/picker-item-active/);
|
||||
});
|
||||
test('defaulting the value to a disabled item should not cause that item to be active', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
<ion-picker>
|
||||
<ion-picker-column></ion-picker-column>
|
||||
</ion-picker>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal');
|
||||
const column = document.querySelector('ion-picker-column');
|
||||
column.items = [
|
||||
{ text: 'A', value: 'a' },
|
||||
{ text: 'B', value: 'b', disabled: true },
|
||||
@@ -151,7 +121,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
config
|
||||
);
|
||||
|
||||
const disabledItem = page.locator('ion-picker-column-internal .picker-item[data-value="b"]');
|
||||
const disabledItem = page.locator('ion-picker-column .picker-item[data-value="b"]');
|
||||
await expect(disabledItem).toBeDisabled();
|
||||
await expect(disabledItem).not.toHaveClass(/picker-item-active/);
|
||||
});
|
||||
@@ -162,16 +132,16 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
* This behavior does not vary across directions.
|
||||
*/
|
||||
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('picker-column-internal: disabled column rendering'), () => {
|
||||
test.describe(title('picker-column: disabled column rendering'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/src/components/picker-column-internal/test/disabled', config);
|
||||
await page.goto('/src/components/picker-column/test/disabled', config);
|
||||
});
|
||||
|
||||
test('disabled column should not have visual regressions', async ({ page }) => {
|
||||
const disabledColumn = page.locator('#column-disabled');
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(disabledColumn).toHaveScreenshot(screenshot('picker-internal-disabled-column'));
|
||||
await expect(disabledColumn).toHaveScreenshot(screenshot('picker-disabled-column'));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -180,9 +150,9 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
* This behavior does not vary across modes/directions.
|
||||
*/
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('picker-column-internal: disabled column'), () => {
|
||||
test.describe(title('picker-column: disabled column'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/src/components/picker-column-internal/test/disabled', config);
|
||||
await page.goto('/src/components/picker-column/test/disabled', config);
|
||||
});
|
||||
|
||||
test('item in disabled column should not be interactive', async ({ page }) => {
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
@@ -1,9 +0,0 @@
|
||||
export interface PickerInternalChangeEventDetail {
|
||||
useInputMode: boolean;
|
||||
inputModeColumn?: HTMLIonPickerColumnInternalElement;
|
||||
}
|
||||
|
||||
export interface PickerInternalCustomEvent extends CustomEvent {
|
||||
target: HTMLIonPickerInternalElement;
|
||||
detail: PickerInternalChangeEventDetail;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
@import "./picker-internal.scss";
|
||||
@import "./picker-internal.vars.scss";
|
||||
@import "../../themes/ionic.globals.ios";
|
||||
|
||||
:host .picker-before {
|
||||
background: linear-gradient(to bottom, rgba(#{$picker-fade-background}, 1) 20%, rgba(#{$picker-fade-background}, 0.8) 100%);
|
||||
}
|
||||
|
||||
:host .picker-after {
|
||||
background: linear-gradient(to top, rgba(#{$picker-fade-background}, 1) 20%, rgba(#{$picker-fade-background}, 0.8) 100%);
|
||||
}
|
||||
|
||||
:host .picker-highlight {
|
||||
background: var(--wheel-highlight-background, var(--ion-color-step-150, #eeeeef));
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
@import "./picker-internal.scss";
|
||||
@import "./picker-internal.vars.scss";
|
||||
@import "../../themes/ionic.globals.md";
|
||||
|
||||
:host .picker-before {
|
||||
background: linear-gradient(to bottom, rgba(#{$picker-fade-background}, 1) 20%, rgba(#{$picker-fade-background}, 0) 90%);
|
||||
}
|
||||
|
||||
:host .picker-after {
|
||||
background: linear-gradient(to top, rgba(#{$picker-fade-background}, 1) 30%, rgba(#{$picker-fade-background}, 0) 90%);
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
@import "../../themes/ionic.globals";
|
||||
|
||||
// Picker Internal
|
||||
// --------------------------------------------------
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
|
||||
position: relative;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
|
||||
/**
|
||||
* Picker columns should display
|
||||
* in the order in which developers
|
||||
* added them and should ignore
|
||||
* LTR vs RTL directions.
|
||||
*/
|
||||
direction: ltr;
|
||||
|
||||
/**
|
||||
* This is required otherwise the
|
||||
* highlight will appear behind
|
||||
* the picker when used inline.
|
||||
*/
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
:host .picker-before,
|
||||
:host .picker-after {
|
||||
position: absolute;
|
||||
|
||||
width: 100%;
|
||||
|
||||
/**
|
||||
* The transform and z-index
|
||||
* are needed for WebKit otherwise
|
||||
* the fade will appear underneath the picker.
|
||||
*/
|
||||
transform: translateZ(0);
|
||||
|
||||
z-index: 1;
|
||||
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:host .picker-before {
|
||||
@include position(0, null, null, 0);
|
||||
|
||||
height: 83px;
|
||||
}
|
||||
|
||||
:host .picker-after {
|
||||
@include position(116px, null, null, 0);
|
||||
|
||||
height: 84px;
|
||||
}
|
||||
|
||||
:host .picker-highlight {
|
||||
@include border-radius(8px, 8px, 8px, 8px);
|
||||
@include position(50%, 0, 0, 0);
|
||||
@include margin(0, auto, 0, auto);
|
||||
|
||||
position: absolute;
|
||||
|
||||
width: calc(100% - 16px);
|
||||
height: 34px;
|
||||
|
||||
transform: translateY(-50%);
|
||||
|
||||
background: var(--wheel-highlight-background);
|
||||
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
:host input {
|
||||
@include visually-hidden();
|
||||
}
|
||||
|
||||
:host ::slotted(ion-picker-column-internal:first-of-type) {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
:host ::slotted(ion-picker-column-internal:last-of-type) {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
:host ::slotted(ion-picker-column-internal:only-child) {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -1,564 +0,0 @@
|
||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { Component, Element, Event, Listen, Method, Host, h } from '@stencil/core';
|
||||
import { getElementRoot } from '@utils/helpers';
|
||||
|
||||
import type { PickerInternalChangeEventDetail } from './picker-internal-interfaces';
|
||||
|
||||
/**
|
||||
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
|
||||
* @internal
|
||||
*/
|
||||
@Component({
|
||||
tag: 'ion-picker-internal',
|
||||
styleUrls: {
|
||||
ios: 'picker-internal.ios.scss',
|
||||
md: 'picker-internal.md.scss',
|
||||
},
|
||||
shadow: true,
|
||||
})
|
||||
export class PickerInternal implements ComponentInterface {
|
||||
private inputEl?: HTMLInputElement;
|
||||
private useInputMode = false;
|
||||
private inputModeColumn?: HTMLIonPickerColumnInternalElement;
|
||||
private highlightEl?: HTMLElement;
|
||||
private actionOnClick?: () => void;
|
||||
private destroyKeypressListener?: () => void;
|
||||
private singleColumnSearchTimeout?: ReturnType<typeof setTimeout>;
|
||||
|
||||
@Element() el!: HTMLIonPickerInternalElement;
|
||||
|
||||
@Event() ionInputModeChange!: EventEmitter<PickerInternalChangeEventDetail>;
|
||||
|
||||
/**
|
||||
* When the picker is interacted with
|
||||
* we need to prevent touchstart so other
|
||||
* gestures do not fire. For example,
|
||||
* scrolling on the wheel picker
|
||||
* in ion-datetime should not cause
|
||||
* a card modal to swipe to close.
|
||||
*/
|
||||
@Listen('touchstart')
|
||||
preventTouchStartPropagation(ev: TouchEvent) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
getElementRoot(this.el).addEventListener('focusin', this.onFocusIn);
|
||||
getElementRoot(this.el).addEventListener('focusout', this.onFocusOut);
|
||||
}
|
||||
|
||||
private isInHighlightBounds = (ev: PointerEvent) => {
|
||||
const { highlightEl } = this;
|
||||
if (!highlightEl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bbox = highlightEl.getBoundingClientRect();
|
||||
/**
|
||||
* Check to see if the user clicked
|
||||
* outside the bounds of the highlight.
|
||||
*/
|
||||
const outsideX = ev.clientX < bbox.left || ev.clientX > bbox.right;
|
||||
const outsideY = ev.clientY < bbox.top || ev.clientY > bbox.bottom;
|
||||
if (outsideX || outsideY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* If we are no longer focused
|
||||
* on a picker column, then we should
|
||||
* exit input mode. An exception is made
|
||||
* for the input in the picker since having
|
||||
* that focused means we are still in input mode.
|
||||
*/
|
||||
private onFocusOut = (ev: any) => {
|
||||
// TODO(FW-2832): type
|
||||
const { relatedTarget } = ev;
|
||||
|
||||
if (!relatedTarget || (relatedTarget.tagName !== 'ION-PICKER-COLUMN-INTERNAL' && relatedTarget !== this.inputEl)) {
|
||||
this.exitInputMode();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* When picker columns receive focus
|
||||
* the parent picker needs to determine
|
||||
* whether to enter/exit input mode.
|
||||
*/
|
||||
private onFocusIn = (ev: any) => {
|
||||
// TODO(FW-2832): type
|
||||
const { target } = ev;
|
||||
|
||||
/**
|
||||
* Due to browser differences in how/when focus
|
||||
* is dispatched on certain elements, we need to
|
||||
* make sure that this function only ever runs when
|
||||
* focusing a picker column.
|
||||
*/
|
||||
if (target.tagName !== 'ION-PICKER-COLUMN-INTERNAL') {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* If we have actionOnClick
|
||||
* then this means the user focused
|
||||
* a picker column via mouse or
|
||||
* touch (i.e. a PointerEvent). As a result,
|
||||
* we should not enter/exit input mode
|
||||
* until the click event has fired, which happens
|
||||
* after the `focusin` event.
|
||||
*
|
||||
* Otherwise, the user likely focused
|
||||
* the column using their keyboard and
|
||||
* we should enter/exit input mode automatically.
|
||||
*/
|
||||
if (!this.actionOnClick) {
|
||||
const columnEl = target as HTMLIonPickerColumnInternalElement;
|
||||
const allowInput = columnEl.numericInput;
|
||||
if (allowInput) {
|
||||
this.enterInputMode(columnEl, false);
|
||||
} else {
|
||||
this.exitInputMode();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* On click we need to run an actionOnClick
|
||||
* function that has been set in onPointerDown
|
||||
* so that we enter/exit input mode correctly.
|
||||
*/
|
||||
private onClick = () => {
|
||||
const { actionOnClick } = this;
|
||||
if (actionOnClick) {
|
||||
actionOnClick();
|
||||
this.actionOnClick = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clicking a column also focuses the column on
|
||||
* certain browsers, so we use onPointerDown
|
||||
* to tell the onFocusIn function that users
|
||||
* are trying to click the column rather than
|
||||
* focus the column using the keyboard. When the
|
||||
* user completes the click, the onClick function
|
||||
* runs and runs the actionOnClick callback.
|
||||
*/
|
||||
private onPointerDown = (ev: PointerEvent) => {
|
||||
const { useInputMode, inputModeColumn, el } = this;
|
||||
if (this.isInHighlightBounds(ev)) {
|
||||
/**
|
||||
* If we were already in
|
||||
* input mode, then we should determine
|
||||
* if we tapped a particular column and
|
||||
* should switch to input mode for
|
||||
* that specific column.
|
||||
*/
|
||||
if (useInputMode) {
|
||||
/**
|
||||
* If we tapped a picker column
|
||||
* then we should either switch to input
|
||||
* mode for that column or all columns.
|
||||
* Otherwise we should exit input mode
|
||||
* since we just tapped the highlight and
|
||||
* not a column.
|
||||
*/
|
||||
if ((ev.target as HTMLElement).tagName === 'ION-PICKER-COLUMN-INTERNAL') {
|
||||
/**
|
||||
* If user taps 2 different columns
|
||||
* then we should just switch to input mode
|
||||
* for the new column rather than switching to
|
||||
* input mode for all columns.
|
||||
*/
|
||||
if (inputModeColumn && inputModeColumn === ev.target) {
|
||||
this.actionOnClick = () => {
|
||||
this.enterInputMode();
|
||||
};
|
||||
} else {
|
||||
this.actionOnClick = () => {
|
||||
this.enterInputMode(ev.target as HTMLIonPickerColumnInternalElement);
|
||||
};
|
||||
}
|
||||
} else {
|
||||
this.actionOnClick = () => {
|
||||
this.exitInputMode();
|
||||
};
|
||||
}
|
||||
/**
|
||||
* If we were not already in
|
||||
* input mode, then we should
|
||||
* enter input mode for all columns.
|
||||
*/
|
||||
} else {
|
||||
/**
|
||||
* If there is only 1 numeric input column
|
||||
* then we should skip multi column input.
|
||||
*/
|
||||
const columns = el.querySelectorAll('ion-picker-column-internal.picker-column-numeric-input');
|
||||
const columnEl = columns.length === 1 ? (ev.target as HTMLIonPickerColumnInternalElement) : undefined;
|
||||
this.actionOnClick = () => {
|
||||
this.enterInputMode(columnEl);
|
||||
};
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionOnClick = () => {
|
||||
this.exitInputMode();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Enters input mode to allow
|
||||
* for text entry of numeric values.
|
||||
* If on mobile, we focus a hidden input
|
||||
* field so that the on screen keyboard
|
||||
* is brought up. When tabbing using a
|
||||
* keyboard, picker columns receive an outline
|
||||
* to indicate they are focused. As a result,
|
||||
* we should not focus the hidden input as it
|
||||
* would cause the outline to go away, preventing
|
||||
* users from having any visual indication of which
|
||||
* column is focused.
|
||||
*/
|
||||
private enterInputMode = (columnEl?: HTMLIonPickerColumnInternalElement, focusInput = true) => {
|
||||
const { inputEl, el } = this;
|
||||
if (!inputEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only active input mode if there is at
|
||||
* least one column that accepts numeric input.
|
||||
*/
|
||||
const hasInputColumn = el.querySelector('ion-picker-column-internal.picker-column-numeric-input');
|
||||
if (!hasInputColumn) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* If columnEl is undefined then
|
||||
* it is assumed that all numeric pickers
|
||||
* are eligible for text entry.
|
||||
* (i.e. hour and minute columns)
|
||||
*/
|
||||
this.useInputMode = true;
|
||||
this.inputModeColumn = columnEl;
|
||||
|
||||
/**
|
||||
* Users with a keyboard and mouse can
|
||||
* activate input mode where the input is
|
||||
* focused as well as when it is not focused,
|
||||
* so we need to make sure we clean up any
|
||||
* old listeners.
|
||||
*/
|
||||
if (focusInput) {
|
||||
if (this.destroyKeypressListener) {
|
||||
this.destroyKeypressListener();
|
||||
this.destroyKeypressListener = undefined;
|
||||
}
|
||||
|
||||
inputEl.focus();
|
||||
} else {
|
||||
el.addEventListener('keypress', this.onKeyPress);
|
||||
this.destroyKeypressListener = () => {
|
||||
el.removeEventListener('keypress', this.onKeyPress);
|
||||
};
|
||||
}
|
||||
|
||||
this.emitInputModeChange();
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Exits text entry mode for the picker
|
||||
* This method blurs the hidden input
|
||||
* and cause the keyboard to dismiss.
|
||||
*/
|
||||
@Method()
|
||||
async exitInputMode() {
|
||||
const { inputEl, useInputMode } = this;
|
||||
if (!useInputMode || !inputEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.useInputMode = false;
|
||||
this.inputModeColumn = undefined;
|
||||
inputEl.blur();
|
||||
inputEl.value = '';
|
||||
|
||||
if (this.destroyKeypressListener) {
|
||||
this.destroyKeypressListener();
|
||||
this.destroyKeypressListener = undefined;
|
||||
}
|
||||
|
||||
this.emitInputModeChange();
|
||||
}
|
||||
|
||||
private onKeyPress = (ev: KeyboardEvent) => {
|
||||
const { inputEl } = this;
|
||||
if (!inputEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedValue = parseInt(ev.key, 10);
|
||||
|
||||
/**
|
||||
* Only numbers should be allowed
|
||||
*/
|
||||
if (!Number.isNaN(parsedValue)) {
|
||||
inputEl.value += ev.key;
|
||||
|
||||
this.onInputChange();
|
||||
}
|
||||
};
|
||||
|
||||
private selectSingleColumn = () => {
|
||||
const { inputEl, inputModeColumn, singleColumnSearchTimeout } = this;
|
||||
if (!inputEl || !inputModeColumn) {
|
||||
return;
|
||||
}
|
||||
|
||||
const values = inputModeColumn.items.filter((item) => item.disabled !== true);
|
||||
|
||||
/**
|
||||
* If users pause for a bit, the search
|
||||
* value should be reset similar to how a
|
||||
* <select> behaves. So typing "34", waiting,
|
||||
* then typing "5" should select "05".
|
||||
*/
|
||||
if (singleColumnSearchTimeout) {
|
||||
clearTimeout(singleColumnSearchTimeout);
|
||||
}
|
||||
|
||||
this.singleColumnSearchTimeout = setTimeout(() => {
|
||||
inputEl.value = '';
|
||||
this.singleColumnSearchTimeout = undefined;
|
||||
}, 1000);
|
||||
|
||||
/**
|
||||
* For values that are longer than 2 digits long
|
||||
* we should shift the value over 1 character
|
||||
* to the left. So typing "456" would result in "56".
|
||||
* TODO: If we want to support more than just
|
||||
* time entry, we should update this value to be
|
||||
* the max length of all of the picker items.
|
||||
*/
|
||||
if (inputEl.value.length >= 3) {
|
||||
const startIndex = inputEl.value.length - 2;
|
||||
const newString = inputEl.value.substring(startIndex);
|
||||
|
||||
inputEl.value = newString;
|
||||
this.selectSingleColumn();
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checking the value of the input gets priority
|
||||
* first. For example, if the value of the input
|
||||
* is "1" and we entered "2", then the complete value
|
||||
* is "12" and we should select hour 12.
|
||||
*
|
||||
* Regex removes any leading zeros from values like "02",
|
||||
* but it keeps a single zero if there are only zeros in the string.
|
||||
* 0+(?=[1-9]) --> Match 1 or more zeros that are followed by 1-9
|
||||
* 0+(?=0$) --> Match 1 or more zeros that must be followed by one 0 and end.
|
||||
*/
|
||||
const findItemFromCompleteValue = values.find(({ text }) => {
|
||||
const parsedText = text.replace(/^0+(?=[1-9])|0+(?=0$)/, '');
|
||||
return parsedText === inputEl.value;
|
||||
});
|
||||
|
||||
if (findItemFromCompleteValue) {
|
||||
inputModeColumn.setValue(findItemFromCompleteValue.value);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* If we typed "56" to get minute 56, then typed "7",
|
||||
* we should select "07" as "567" is not a valid minute.
|
||||
*/
|
||||
if (inputEl.value.length === 2) {
|
||||
const changedCharacter = inputEl.value.substring(inputEl.value.length - 1);
|
||||
inputEl.value = changedCharacter;
|
||||
this.selectSingleColumn();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Searches a list of column items for a particular
|
||||
* value. This is currently used for numeric values.
|
||||
* The zeroBehavior can be set to account for leading
|
||||
* or trailing zeros when looking at the item text.
|
||||
*/
|
||||
private searchColumn = (
|
||||
colEl: HTMLIonPickerColumnInternalElement,
|
||||
value: string,
|
||||
zeroBehavior: 'start' | 'end' = 'start'
|
||||
) => {
|
||||
const behavior = zeroBehavior === 'start' ? /^0+/ : /0$/;
|
||||
const item = colEl.items.find(({ text, disabled }) => disabled !== true && text.replace(behavior, '') === value);
|
||||
|
||||
if (item) {
|
||||
colEl.setValue(item.value);
|
||||
}
|
||||
};
|
||||
|
||||
private selectMultiColumn = () => {
|
||||
const { inputEl, el } = this;
|
||||
if (!inputEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const numericPickers = Array.from(el.querySelectorAll('ion-picker-column-internal')).filter(
|
||||
(col) => col.numericInput
|
||||
);
|
||||
|
||||
const firstColumn = numericPickers[0];
|
||||
const lastColumn = numericPickers[1];
|
||||
|
||||
let value = inputEl.value;
|
||||
let minuteValue;
|
||||
switch (value.length) {
|
||||
case 1:
|
||||
this.searchColumn(firstColumn, value);
|
||||
break;
|
||||
case 2:
|
||||
/**
|
||||
* If the first character is `0` or `1` it is
|
||||
* possible that users are trying to type `09`
|
||||
* or `11` into the hour field, so we should look
|
||||
* at that first.
|
||||
*/
|
||||
const firstCharacter = inputEl.value.substring(0, 1);
|
||||
value = firstCharacter === '0' || firstCharacter === '1' ? inputEl.value : firstCharacter;
|
||||
|
||||
this.searchColumn(firstColumn, value);
|
||||
|
||||
/**
|
||||
* If only checked the first value,
|
||||
* we can check the second value
|
||||
* for a match in the minutes column
|
||||
*/
|
||||
if (value.length === 1) {
|
||||
minuteValue = inputEl.value.substring(inputEl.value.length - 1);
|
||||
this.searchColumn(lastColumn, minuteValue, 'end');
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
/**
|
||||
* If the first character is `0` or `1` it is
|
||||
* possible that users are trying to type `09`
|
||||
* or `11` into the hour field, so we should look
|
||||
* at that first.
|
||||
*/
|
||||
const firstCharacterAgain = inputEl.value.substring(0, 1);
|
||||
value =
|
||||
firstCharacterAgain === '0' || firstCharacterAgain === '1'
|
||||
? inputEl.value.substring(0, 2)
|
||||
: firstCharacterAgain;
|
||||
|
||||
this.searchColumn(firstColumn, value);
|
||||
|
||||
/**
|
||||
* If only checked the first value,
|
||||
* we can check the second value
|
||||
* for a match in the minutes column
|
||||
*/
|
||||
minuteValue = value.length === 1 ? inputEl.value.substring(1) : inputEl.value.substring(2);
|
||||
|
||||
this.searchColumn(lastColumn, minuteValue, 'end');
|
||||
break;
|
||||
case 4:
|
||||
/**
|
||||
* If the first character is `0` or `1` it is
|
||||
* possible that users are trying to type `09`
|
||||
* or `11` into the hour field, so we should look
|
||||
* at that first.
|
||||
*/
|
||||
const firstCharacterAgainAgain = inputEl.value.substring(0, 1);
|
||||
value =
|
||||
firstCharacterAgainAgain === '0' || firstCharacterAgainAgain === '1'
|
||||
? inputEl.value.substring(0, 2)
|
||||
: firstCharacterAgainAgain;
|
||||
this.searchColumn(firstColumn, value);
|
||||
|
||||
/**
|
||||
* If only checked the first value,
|
||||
* we can check the second value
|
||||
* for a match in the minutes column
|
||||
*/
|
||||
const minuteValueAgain =
|
||||
value.length === 1
|
||||
? inputEl.value.substring(1, inputEl.value.length)
|
||||
: inputEl.value.substring(2, inputEl.value.length);
|
||||
this.searchColumn(lastColumn, minuteValueAgain, 'end');
|
||||
|
||||
break;
|
||||
default:
|
||||
const startIndex = inputEl.value.length - 4;
|
||||
const newString = inputEl.value.substring(startIndex);
|
||||
|
||||
inputEl.value = newString;
|
||||
this.selectMultiColumn();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Searches the value of the active column
|
||||
* to determine which value users are trying
|
||||
* to select
|
||||
*/
|
||||
private onInputChange = () => {
|
||||
const { useInputMode, inputEl, inputModeColumn } = this;
|
||||
if (!useInputMode || !inputEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputModeColumn) {
|
||||
this.selectSingleColumn();
|
||||
} else {
|
||||
this.selectMultiColumn();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Emit ionInputModeChange. Picker columns
|
||||
* listen for this event to determine whether
|
||||
* or not their column is "active" for text input.
|
||||
*/
|
||||
private emitInputModeChange = () => {
|
||||
const { useInputMode, inputModeColumn } = this;
|
||||
|
||||
this.ionInputModeChange.emit({
|
||||
useInputMode,
|
||||
inputModeColumn,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Host onPointerDown={(ev: PointerEvent) => this.onPointerDown(ev)} onClick={() => this.onClick()}>
|
||||
<input
|
||||
aria-hidden="true"
|
||||
tabindex={-1}
|
||||
inputmode="numeric"
|
||||
type="number"
|
||||
ref={(el) => (this.inputEl = el)}
|
||||
onInput={() => this.onInputChange()}
|
||||
onBlur={() => this.exitInputMode()}
|
||||
/>
|
||||
<div class="picker-before"></div>
|
||||
<div class="picker-after"></div>
|
||||
<div class="picker-highlight" ref={(el) => (this.highlightEl = el)}></div>
|
||||
<slot></slot>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
$picker-fade-background-fallback: var(--background-rgb, var(--ion-background-color-rgb, 255, 255, 255));
|
||||
$picker-fade-background: var(--wheel-fade-background-rgb, $picker-fade-background-fallback);
|
||||
@@ -1,239 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Picker - Basic</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0" />
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
<style>
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(250px, 1fr));
|
||||
grid-row-gap: 20px;
|
||||
grid-column-gap: 20px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
|
||||
color: #6f7378;
|
||||
|
||||
margin-top: 10px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ion-popover {
|
||||
--width: 300px;
|
||||
}
|
||||
|
||||
ion-modal {
|
||||
--height: 250px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Picker - Basic</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding">
|
||||
<div class="grid">
|
||||
<div class="grid-item">
|
||||
<h2>Inline</h2>
|
||||
<ion-picker-internal id="inline">
|
||||
<ion-picker-column-internal id="first"></ion-picker-column-internal>
|
||||
<ion-picker-column-internal id="second"></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<h2>One Numeric Input</h2>
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal numeric-input="true" id="numeric-first"></ion-picker-column-internal>
|
||||
<ion-picker-column-internal id="numeric-second"></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<h2>Two Numeric Input</h2>
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal numeric-input="true" id="dual-numeric-first"></ion-picker-column-internal>
|
||||
<ion-picker-column-internal numeric-input="true" id="dual-numeric-second"></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<h2>Popover</h2>
|
||||
<ion-button id="popover">Open Picker</ion-button>
|
||||
<ion-popover trigger="popover">
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal id="popover-first"></ion-picker-column-internal>
|
||||
<ion-picker-column-internal id="popover-second"></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
</ion-popover>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<h2>Modal</h2>
|
||||
<ion-button id="modal">Open Modal</ion-button>
|
||||
<ion-modal trigger="modal">
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button>Done</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content force-overscroll="false">
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal id="modal-first"></ion-picker-column-internal>
|
||||
<ion-picker-column-internal id="modal-second"></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
</ion-content>
|
||||
</ion-modal>
|
||||
</div>
|
||||
</div>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
|
||||
<script>
|
||||
const column = document.querySelector('ion-picker-column-internal#numeric-first');
|
||||
column.addEventListener('ionChange', (ev) => {
|
||||
console.log('Column change', ev.detail);
|
||||
});
|
||||
const setPickerColumn = (selector, items, value) => {
|
||||
const picker = document.querySelector(selector);
|
||||
|
||||
picker.items = items;
|
||||
picker.value = value;
|
||||
};
|
||||
|
||||
const modal = document.querySelector('ion-modal');
|
||||
modal.breakpoints = [0, 1];
|
||||
modal.initialBreakpoint = 1;
|
||||
|
||||
setPickerColumn(
|
||||
'#first',
|
||||
[
|
||||
{ text: 'Minified', value: 'minified' },
|
||||
{ text: 'Responsive', value: 'responsive' },
|
||||
{ text: 'Full Stack', value: 'full-stack' },
|
||||
{ text: 'Mobile First', value: 'mobile-first' },
|
||||
{ text: 'Serverless', value: 'serverless' },
|
||||
],
|
||||
'full-stack'
|
||||
);
|
||||
|
||||
setPickerColumn(
|
||||
'#second',
|
||||
[
|
||||
{ text: 'Tomato', value: 'tomato' },
|
||||
{ text: 'Avocado', value: 'avocado' },
|
||||
{ text: 'Onion', value: 'onion' },
|
||||
{ text: 'Potato', value: 'potato' },
|
||||
{ text: 'Artichoke', value: 'artichoke' },
|
||||
],
|
||||
'onion'
|
||||
);
|
||||
|
||||
let minutes = [];
|
||||
for (let i = 1; i <= 60; i++) {
|
||||
const text = i < 10 ? `0${i}` : `${i}`;
|
||||
minutes.push({
|
||||
text,
|
||||
value: i,
|
||||
});
|
||||
}
|
||||
setPickerColumn('#numeric-first', minutes, 3);
|
||||
|
||||
setPickerColumn(
|
||||
'#numeric-second',
|
||||
[
|
||||
{ text: 'Tomatoes', value: 'tomato' },
|
||||
{ text: 'Avocados', value: 'avocado' },
|
||||
{ text: 'Onions', value: 'onion' },
|
||||
{ text: 'Potatoes', value: 'potato' },
|
||||
{ text: 'Artichokes', value: 'artichoke' },
|
||||
],
|
||||
'onion'
|
||||
);
|
||||
|
||||
setPickerColumn(
|
||||
'#dual-numeric-first',
|
||||
[
|
||||
{ text: '12', value: 12 },
|
||||
{ text: '01', value: 1 },
|
||||
{ text: '02', value: 2 },
|
||||
{ text: '03', value: 3 },
|
||||
{ text: '04', value: 4 },
|
||||
{ text: '05', value: 5 },
|
||||
{ text: '06', value: 6 },
|
||||
{ text: '07', value: 7 },
|
||||
{ text: '08', value: 8 },
|
||||
{ text: '09', value: 9 },
|
||||
{ text: '10', value: 10 },
|
||||
{ text: '11', value: 11 },
|
||||
],
|
||||
3
|
||||
);
|
||||
setPickerColumn('#dual-numeric-second', minutes, 3);
|
||||
|
||||
setPickerColumn(
|
||||
'#popover-first',
|
||||
[
|
||||
{ text: 'Minified', value: 'minified' },
|
||||
{ text: 'Responsive', value: 'responsive' },
|
||||
{ text: 'Full Stack', value: 'full-stack' },
|
||||
{ text: 'Mobile First', value: 'mobile-first' },
|
||||
{ text: 'Serverless', value: 'serverless' },
|
||||
],
|
||||
'full-stack'
|
||||
);
|
||||
|
||||
setPickerColumn(
|
||||
'#popover-second',
|
||||
[
|
||||
{ text: 'Tomato', value: 'tomato' },
|
||||
{ text: 'Avocado', value: 'avocado' },
|
||||
{ text: 'Onion', value: 'onion' },
|
||||
{ text: 'Potato', value: 'potato' },
|
||||
{ text: 'Artichoke', value: 'artichoke' },
|
||||
],
|
||||
'onion'
|
||||
);
|
||||
|
||||
setPickerColumn(
|
||||
'#modal-first',
|
||||
[
|
||||
{ text: 'Minified', value: 'minified' },
|
||||
{ text: 'Responsive', value: 'responsive' },
|
||||
{ text: 'Full Stack', value: 'full-stack' },
|
||||
{ text: 'Mobile First', value: 'mobile-first' },
|
||||
{ text: 'Serverless', value: 'serverless' },
|
||||
],
|
||||
'full-stack'
|
||||
);
|
||||
|
||||
setPickerColumn(
|
||||
'#modal-second',
|
||||
[
|
||||
{ text: 'Tomato', value: 'tomato' },
|
||||
{ text: 'Avocado', value: 'avocado' },
|
||||
{ text: 'Onion', value: 'onion' },
|
||||
{ text: 'Potato', value: 'potato' },
|
||||
{ text: 'Artichoke', value: 'artichoke' },
|
||||
],
|
||||
'onion'
|
||||
);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,144 +0,0 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This behavior does not vary across directions.
|
||||
*/
|
||||
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('picker-internal: rendering'), () => {
|
||||
test('inline pickers should not have visual regression', async ({ page }) => {
|
||||
await page.goto(`/src/components/picker-internal/test/basic`, config);
|
||||
|
||||
const fullStack = page.locator('#inline button[data-value="full-stack"]');
|
||||
const onion = page.locator('#inline button[data-value="onion"]');
|
||||
|
||||
await expect(fullStack).toHaveClass(/picker-item-active/);
|
||||
await expect(onion).toHaveClass(/picker-item-active/);
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(page.locator('#inline')).toHaveScreenshot(screenshot(`picker-internal-inline-diff`));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe(title('picker-internal: overlay rendering'), () => {
|
||||
test('popover: should not have visual regression', async ({ page }) => {
|
||||
await page.goto(`/src/components/picker-internal/test/basic`, config);
|
||||
|
||||
const button = page.locator('#popover');
|
||||
const didPresent = await page.spyOnEvent('ionPopoverDidPresent');
|
||||
const pickerInternal = page.locator('ion-popover ion-picker-internal');
|
||||
|
||||
await button.click();
|
||||
await didPresent.next();
|
||||
|
||||
await expect(pickerInternal).toBeVisible();
|
||||
|
||||
const popoverContent = page.locator('ion-popover .ion-delegate-host');
|
||||
|
||||
await expect(popoverContent).toHaveScreenshot(screenshot(`picker-internal-popover-diff`), {
|
||||
/**
|
||||
* Animations must be enabled to capture the screenshot.
|
||||
* By default, animations are disabled with toHaveScreenshot,
|
||||
* and when capturing the screenshot will call animation.finish().
|
||||
* This will cause the popover to close and the screenshot capture
|
||||
* to be invalid.
|
||||
*/
|
||||
animations: 'allow',
|
||||
});
|
||||
});
|
||||
|
||||
test('modal: should not have visual regression', async ({ page }) => {
|
||||
await page.goto('/src/components/picker-internal/test/basic', config);
|
||||
|
||||
const button = page.locator('#modal');
|
||||
const didPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
const pickerInternal = page.locator('ion-modal ion-picker-internal');
|
||||
|
||||
await button.click();
|
||||
await didPresent.next();
|
||||
|
||||
await expect(pickerInternal).toBeVisible();
|
||||
|
||||
const modalContent = page.locator('ion-modal .ion-delegate-host');
|
||||
|
||||
await expect(modalContent).toHaveScreenshot(screenshot(`picker-internal-modal-diff`), {
|
||||
/**
|
||||
* Animations must be enabled to capture the screenshot.
|
||||
* By default, animations are disabled with toHaveScreenshot,
|
||||
* and when capturing the screenshot will call animation.finish().
|
||||
* This will cause the modal to close and the screenshot capture
|
||||
* to be invalid.
|
||||
*/
|
||||
animations: 'allow',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This behavior does not vary across modes/directions.
|
||||
*/
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('picker-internal: focus'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-picker-internal>
|
||||
<ion-picker-column-internal value="full-stack" id="first"></ion-picker-column-internal>
|
||||
<ion-picker-column-internal value="onion" id="second"></ion-picker-column-internal>
|
||||
</ion-picker-internal>
|
||||
|
||||
<script>
|
||||
const columns = document.querySelectorAll('ion-picker-column-internal');
|
||||
columns[0].items = [
|
||||
{ text: 'Minified', value: 'minified' },
|
||||
{ text: 'Responsive', value: 'responsive' },
|
||||
{ text: 'Full Stack', value: 'full-stack' },
|
||||
{ text: 'Mobile First', value: 'mobile-first' },
|
||||
{ text: 'Serverless', value: 'serverless' },
|
||||
]
|
||||
|
||||
columns[1].items = [
|
||||
{ text: 'Tomato', value: 'tomato' },
|
||||
{ text: 'Avocado', value: 'avocado' },
|
||||
{ text: 'Onion', value: 'onion' },
|
||||
{ text: 'Potato', value: 'potato' },
|
||||
{ text: 'Artichoke', value: 'artichoke' },
|
||||
];
|
||||
</script>
|
||||
`,
|
||||
config
|
||||
);
|
||||
});
|
||||
|
||||
test('tabbing should correctly move focus between columns', async ({ page }) => {
|
||||
const firstColumn = page.locator('ion-picker-column-internal#first');
|
||||
const secondColumn = page.locator('ion-picker-column-internal#second');
|
||||
|
||||
// Focus first column
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(firstColumn).toBeFocused();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
// Focus second column
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(secondColumn).toBeFocused();
|
||||
});
|
||||
|
||||
test('tabbing should correctly move focus back', async ({ page }) => {
|
||||
const firstColumn = page.locator('ion-picker-column-internal#first');
|
||||
const secondColumn = page.locator('ion-picker-column-internal#second');
|
||||
|
||||
await secondColumn.focus();
|
||||
await expect(secondColumn).toBeFocused();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
// Focus first column
|
||||
await page.keyboard.press('Shift+Tab');
|
||||
await expect(firstColumn).toBeFocused();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
@import "./picker-column";
|
||||
@import "../picker-legacy/picker.ios.vars";
|
||||
|
||||
// iOS Picker Column
|
||||
// --------------------------------------------------
|
||||
|
||||
.picker-col {
|
||||
@include padding($picker-ios-column-padding-top, $picker-ios-column-padding-end, $picker-ios-column-padding-bottom, $picker-ios-column-padding-start);
|
||||
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.picker-prefix,
|
||||
.picker-suffix,
|
||||
.picker-opts {
|
||||
top: $picker-ios-option-offset-y;
|
||||
|
||||
transform-style: preserve-3d;
|
||||
|
||||
color: inherit;
|
||||
|
||||
font-size: $picker-ios-option-font-size;
|
||||
|
||||
line-height: $picker-ios-option-height;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.picker-opt {
|
||||
@include padding($picker-ios-option-padding-top, $picker-ios-option-padding-end, $picker-ios-option-padding-bottom, $picker-ios-option-padding-start);
|
||||
@include margin(0);
|
||||
@include transform-origin(center, center);
|
||||
|
||||
height: 46px;
|
||||
|
||||
transform-style: preserve-3d;
|
||||
|
||||
transition-timing-function: ease-out;
|
||||
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
|
||||
font-size: $picker-ios-option-font-size;
|
||||
|
||||
line-height: $picker-ios-option-height;
|
||||
backface-visibility: hidden;
|
||||
pointer-events: auto;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
@import "./picker-column";
|
||||
@import "../picker-legacy/picker.md.vars";
|
||||
|
||||
// Material Design Picker Column
|
||||
// --------------------------------------------------
|
||||
|
||||
.picker-col {
|
||||
@include padding($picker-md-column-padding-top, $picker-md-column-padding-end, $picker-md-column-padding-bottom, $picker-md-column-padding-start);
|
||||
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.picker-prefix,
|
||||
.picker-suffix,
|
||||
.picker-opts {
|
||||
top: $picker-md-option-offset-y;
|
||||
|
||||
transform-style: preserve-3d;
|
||||
|
||||
color: inherit;
|
||||
|
||||
font-size: $picker-md-option-font-size;
|
||||
|
||||
line-height: $picker-md-option-height;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
.picker-opt {
|
||||
@include margin(0);
|
||||
@include padding($picker-md-option-padding-top, $picker-md-option-padding-end, $picker-md-option-padding-bottom, $picker-md-option-padding-start);
|
||||
|
||||
height: 43px;
|
||||
|
||||
transition-timing-function: ease-out;
|
||||
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
|
||||
font-size: $picker-md-option-font-size;
|
||||
|
||||
line-height: $picker-md-option-height;
|
||||
backface-visibility: hidden;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.picker-prefix,
|
||||
.picker-suffix,
|
||||
.picker-opt.picker-opt-selected {
|
||||
color: $picker-md-option-selected-color;
|
||||
}
|
||||
89
core/src/components/picker-legacy-column/picker-column.scss
Normal file
@@ -0,0 +1,89 @@
|
||||
@import "../../themes/ionic.globals";
|
||||
|
||||
// Picker Column
|
||||
// --------------------------------------------------
|
||||
|
||||
.picker-col {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
|
||||
height: 100%;
|
||||
box-sizing: content-box;
|
||||
|
||||
contain: content;
|
||||
}
|
||||
|
||||
.picker-opts {
|
||||
position: relative;
|
||||
|
||||
flex: 1;
|
||||
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
// contain property is supported by Chrome
|
||||
.picker-opt {
|
||||
@include position(0, null, null, 0);
|
||||
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
||||
width: 100%;
|
||||
|
||||
border: 0;
|
||||
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
contain: strict;
|
||||
overflow: hidden;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.picker-opt.picker-opt-disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.picker-opt-disabled {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.picker-opts-left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.picker-opts-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.picker-opt {
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.picker-prefix {
|
||||
position: relative;
|
||||
|
||||
flex: 1;
|
||||
|
||||
text-align: end;
|
||||
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.picker-suffix {
|
||||
position: relative;
|
||||
|
||||
flex: 1;
|
||||
|
||||
text-align: start;
|
||||
|
||||
white-space: nowrap;
|
||||
}
|
||||
440
core/src/components/picker-legacy-column/picker-column.tsx
Normal file
@@ -0,0 +1,440 @@
|
||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { Component, Element, Event, Host, Prop, Watch, h } from '@stencil/core';
|
||||
import { clamp } from '@utils/helpers';
|
||||
import { hapticSelectionChanged, hapticSelectionEnd, hapticSelectionStart } from '@utils/native/haptic';
|
||||
import { getClassMap } from '@utils/theme';
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
import type { Gesture, GestureDetail } from '../../interface';
|
||||
import type { PickerColumn } from '../picker-legacy/picker-interface';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@Component({
|
||||
tag: 'ion-picker-legacy-column',
|
||||
styleUrls: {
|
||||
ios: 'picker-column.ios.scss',
|
||||
md: 'picker-column.md.scss',
|
||||
},
|
||||
})
|
||||
export class PickerColumnCmp implements ComponentInterface {
|
||||
private bounceFrom!: number;
|
||||
private lastIndex?: number;
|
||||
private minY!: number;
|
||||
private maxY!: number;
|
||||
private optHeight = 0;
|
||||
private rotateFactor = 0;
|
||||
private scaleFactor = 1;
|
||||
private velocity = 0;
|
||||
private y = 0;
|
||||
private optsEl?: HTMLElement;
|
||||
private gesture?: Gesture;
|
||||
private rafId?: ReturnType<typeof requestAnimationFrame>;
|
||||
private tmrId?: ReturnType<typeof setTimeout>;
|
||||
private noAnimate = true;
|
||||
// `colDidChange` is a flag that gets set when the column is changed
|
||||
// dynamically. When this flag is set, the column will refresh
|
||||
// after the component re-renders to incorporate the new column data.
|
||||
// This is necessary because `this.refresh` queries for the option elements,
|
||||
// so it needs to wait for the latest elements to be available in the DOM.
|
||||
// Ex: column is created with 3 options. User updates the column data
|
||||
// to have 5 options. The column will still think it only has 3 options.
|
||||
private colDidChange = false;
|
||||
|
||||
@Element() el!: HTMLElement;
|
||||
|
||||
/**
|
||||
* Emitted when the selected value has changed
|
||||
* @internal
|
||||
*/
|
||||
@Event() ionPickerColChange!: EventEmitter<PickerColumn>;
|
||||
|
||||
/** Picker column data */
|
||||
@Prop() col!: PickerColumn;
|
||||
@Watch('col')
|
||||
protected colChanged() {
|
||||
this.colDidChange = true;
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
let pickerRotateFactor = 0;
|
||||
let pickerScaleFactor = 0.81;
|
||||
|
||||
const mode = getIonMode(this);
|
||||
|
||||
if (mode === 'ios') {
|
||||
pickerRotateFactor = -0.46;
|
||||
pickerScaleFactor = 1;
|
||||
}
|
||||
|
||||
this.rotateFactor = pickerRotateFactor;
|
||||
this.scaleFactor = pickerScaleFactor;
|
||||
|
||||
this.gesture = (await import('../../utils/gesture')).createGesture({
|
||||
el: this.el,
|
||||
gestureName: 'picker-swipe',
|
||||
gesturePriority: 100,
|
||||
threshold: 0,
|
||||
passive: false,
|
||||
onStart: (ev) => this.onStart(ev),
|
||||
onMove: (ev) => this.onMove(ev),
|
||||
onEnd: (ev) => this.onEnd(ev),
|
||||
});
|
||||
this.gesture.enable();
|
||||
// Options have not been initialized yet
|
||||
// Animation must be disabled through the `noAnimate` flag
|
||||
// Otherwise, the options will render
|
||||
// at the top of the column and transition down
|
||||
this.tmrId = setTimeout(() => {
|
||||
this.noAnimate = false;
|
||||
// After initialization, `refresh()` will be called
|
||||
// At this point, animation will be enabled. The options will
|
||||
// animate as they are being selected.
|
||||
this.refresh(true);
|
||||
}, 250);
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
this.onDomChange();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
// Options may have changed since last update.
|
||||
if (this.colDidChange) {
|
||||
// Animation must be disabled through the `onDomChange` parameter.
|
||||
// Otherwise, the recently added options will render
|
||||
// at the top of the column and transition down
|
||||
this.onDomChange(true, false);
|
||||
this.colDidChange = false;
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this.rafId !== undefined) cancelAnimationFrame(this.rafId);
|
||||
if (this.tmrId) clearTimeout(this.tmrId);
|
||||
if (this.gesture) {
|
||||
this.gesture.destroy();
|
||||
this.gesture = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private emitColChange() {
|
||||
this.ionPickerColChange.emit(this.col);
|
||||
}
|
||||
|
||||
private setSelected(selectedIndex: number, duration: number) {
|
||||
// if there is a selected index, then figure out it's y position
|
||||
// if there isn't a selected index, then just use the top y position
|
||||
const y = selectedIndex > -1 ? -(selectedIndex * this.optHeight) : 0;
|
||||
|
||||
this.velocity = 0;
|
||||
|
||||
// set what y position we're at
|
||||
if (this.rafId !== undefined) cancelAnimationFrame(this.rafId);
|
||||
this.update(y, duration, true);
|
||||
|
||||
this.emitColChange();
|
||||
}
|
||||
|
||||
private update(y: number, duration: number, saveY: boolean) {
|
||||
if (!this.optsEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure we've got a good round number :)
|
||||
let translateY = 0;
|
||||
let translateZ = 0;
|
||||
const { col, rotateFactor } = this;
|
||||
const prevSelected = col.selectedIndex;
|
||||
const selectedIndex = (col.selectedIndex = this.indexForY(-y));
|
||||
const durationStr = duration === 0 ? '' : duration + 'ms';
|
||||
const scaleStr = `scale(${this.scaleFactor})`;
|
||||
|
||||
const children = this.optsEl.children;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const button = children[i] as HTMLElement;
|
||||
const opt = col.options[i];
|
||||
const optOffset = i * this.optHeight + y;
|
||||
let transform = '';
|
||||
|
||||
if (rotateFactor !== 0) {
|
||||
const rotateX = optOffset * rotateFactor;
|
||||
if (Math.abs(rotateX) <= 90) {
|
||||
translateY = 0;
|
||||
translateZ = 90;
|
||||
transform = `rotateX(${rotateX}deg) `;
|
||||
} else {
|
||||
translateY = -9999;
|
||||
}
|
||||
} else {
|
||||
translateZ = 0;
|
||||
translateY = optOffset;
|
||||
}
|
||||
|
||||
const selected = selectedIndex === i;
|
||||
transform += `translate3d(0px,${translateY}px,${translateZ}px) `;
|
||||
if (this.scaleFactor !== 1 && !selected) {
|
||||
transform += scaleStr;
|
||||
}
|
||||
|
||||
// Update transition duration
|
||||
if (this.noAnimate) {
|
||||
opt.duration = 0;
|
||||
button.style.transitionDuration = '';
|
||||
} else if (duration !== opt.duration) {
|
||||
opt.duration = duration;
|
||||
button.style.transitionDuration = durationStr;
|
||||
}
|
||||
|
||||
// Update transform
|
||||
if (transform !== opt.transform) {
|
||||
opt.transform = transform;
|
||||
}
|
||||
button.style.transform = transform;
|
||||
|
||||
/**
|
||||
* Ensure that the select column
|
||||
* item has the selected class
|
||||
*/
|
||||
opt.selected = selected;
|
||||
|
||||
if (selected) {
|
||||
button.classList.add(PICKER_OPT_SELECTED);
|
||||
} else {
|
||||
button.classList.remove(PICKER_OPT_SELECTED);
|
||||
}
|
||||
}
|
||||
this.col.prevSelected = prevSelected;
|
||||
|
||||
if (saveY) {
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
if (this.lastIndex !== selectedIndex) {
|
||||
// have not set a last index yet
|
||||
hapticSelectionChanged();
|
||||
this.lastIndex = selectedIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private decelerate() {
|
||||
if (this.velocity !== 0) {
|
||||
// still decelerating
|
||||
this.velocity *= DECELERATION_FRICTION;
|
||||
|
||||
// do not let it go slower than a velocity of 1
|
||||
this.velocity = this.velocity > 0 ? Math.max(this.velocity, 1) : Math.min(this.velocity, -1);
|
||||
|
||||
let y = this.y + this.velocity;
|
||||
|
||||
if (y > this.minY) {
|
||||
// whoops, it's trying to scroll up farther than the options we have!
|
||||
y = this.minY;
|
||||
this.velocity = 0;
|
||||
} else if (y < this.maxY) {
|
||||
// gahh, it's trying to scroll down farther than we can!
|
||||
y = this.maxY;
|
||||
this.velocity = 0;
|
||||
}
|
||||
|
||||
this.update(y, 0, true);
|
||||
const notLockedIn = Math.round(y) % this.optHeight !== 0 || Math.abs(this.velocity) > 1;
|
||||
if (notLockedIn) {
|
||||
// isn't locked in yet, keep decelerating until it is
|
||||
this.rafId = requestAnimationFrame(() => this.decelerate());
|
||||
} else {
|
||||
this.velocity = 0;
|
||||
this.emitColChange();
|
||||
hapticSelectionEnd();
|
||||
}
|
||||
} else if (this.y % this.optHeight !== 0) {
|
||||
// needs to still get locked into a position so options line up
|
||||
const currentPos = Math.abs(this.y % this.optHeight);
|
||||
|
||||
// create a velocity in the direction it needs to scroll
|
||||
this.velocity = currentPos > this.optHeight / 2 ? 1 : -1;
|
||||
|
||||
this.decelerate();
|
||||
}
|
||||
}
|
||||
|
||||
private indexForY(y: number) {
|
||||
return Math.min(Math.max(Math.abs(Math.round(y / this.optHeight)), 0), this.col.options.length - 1);
|
||||
}
|
||||
|
||||
private onStart(detail: GestureDetail) {
|
||||
// We have to prevent default in order to block scrolling under the picker
|
||||
// but we DO NOT have to stop propagation, since we still want
|
||||
// some "click" events to capture
|
||||
if (detail.event.cancelable) {
|
||||
detail.event.preventDefault();
|
||||
}
|
||||
detail.event.stopPropagation();
|
||||
|
||||
hapticSelectionStart();
|
||||
|
||||
// reset everything
|
||||
if (this.rafId !== undefined) cancelAnimationFrame(this.rafId);
|
||||
const options = this.col.options;
|
||||
let minY = options.length - 1;
|
||||
let maxY = 0;
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
if (!options[i].disabled) {
|
||||
minY = Math.min(minY, i);
|
||||
maxY = Math.max(maxY, i);
|
||||
}
|
||||
}
|
||||
|
||||
this.minY = -(minY * this.optHeight);
|
||||
this.maxY = -(maxY * this.optHeight);
|
||||
}
|
||||
|
||||
private onMove(detail: GestureDetail) {
|
||||
if (detail.event.cancelable) {
|
||||
detail.event.preventDefault();
|
||||
}
|
||||
detail.event.stopPropagation();
|
||||
|
||||
// update the scroll position relative to pointer start position
|
||||
let y = this.y + detail.deltaY;
|
||||
|
||||
if (y > this.minY) {
|
||||
// scrolling up higher than scroll area
|
||||
y = Math.pow(y, 0.8);
|
||||
this.bounceFrom = y;
|
||||
} else if (y < this.maxY) {
|
||||
// scrolling down below scroll area
|
||||
y += Math.pow(this.maxY - y, 0.9);
|
||||
this.bounceFrom = y;
|
||||
} else {
|
||||
this.bounceFrom = 0;
|
||||
}
|
||||
|
||||
this.update(y, 0, false);
|
||||
}
|
||||
|
||||
private onEnd(detail: GestureDetail) {
|
||||
if (this.bounceFrom > 0) {
|
||||
// bounce back up
|
||||
this.update(this.minY, 100, true);
|
||||
this.emitColChange();
|
||||
return;
|
||||
} else if (this.bounceFrom < 0) {
|
||||
// bounce back down
|
||||
this.update(this.maxY, 100, true);
|
||||
this.emitColChange();
|
||||
return;
|
||||
}
|
||||
|
||||
this.velocity = clamp(-MAX_PICKER_SPEED, detail.velocityY * 23, MAX_PICKER_SPEED);
|
||||
if (this.velocity === 0 && detail.deltaY === 0) {
|
||||
const opt = (detail.event.target as Element).closest('.picker-opt');
|
||||
if (opt?.hasAttribute('opt-index')) {
|
||||
this.setSelected(parseInt(opt.getAttribute('opt-index')!, 10), TRANSITION_DURATION);
|
||||
}
|
||||
} else {
|
||||
this.y += detail.deltaY;
|
||||
|
||||
if (Math.abs(detail.velocityY) < 0.05) {
|
||||
const isScrollingUp = detail.deltaY > 0;
|
||||
const optHeightFraction = (Math.abs(this.y) % this.optHeight) / this.optHeight;
|
||||
|
||||
if (isScrollingUp && optHeightFraction > 0.5) {
|
||||
this.velocity = Math.abs(this.velocity) * -1;
|
||||
} else if (!isScrollingUp && optHeightFraction <= 0.5) {
|
||||
this.velocity = Math.abs(this.velocity);
|
||||
}
|
||||
}
|
||||
|
||||
this.decelerate();
|
||||
}
|
||||
}
|
||||
|
||||
private refresh(forceRefresh?: boolean, animated?: boolean) {
|
||||
let min = this.col.options.length - 1;
|
||||
let max = 0;
|
||||
const options = this.col.options;
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
if (!options[i].disabled) {
|
||||
min = Math.min(min, i);
|
||||
max = Math.max(max, i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only update selected value if column has a
|
||||
* velocity of 0. If it does not, then the
|
||||
* column is animating might land on
|
||||
* a value different than the value at
|
||||
* selectedIndex
|
||||
*/
|
||||
if (this.velocity !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIndex = clamp(min, this.col.selectedIndex ?? 0, max);
|
||||
if (this.col.prevSelected !== selectedIndex || forceRefresh) {
|
||||
const y = selectedIndex * this.optHeight * -1;
|
||||
const duration = animated ? TRANSITION_DURATION : 0;
|
||||
this.velocity = 0;
|
||||
this.update(y, duration, true);
|
||||
}
|
||||
}
|
||||
|
||||
private onDomChange(forceRefresh?: boolean, animated?: boolean) {
|
||||
const colEl = this.optsEl;
|
||||
if (colEl) {
|
||||
// DOM READ
|
||||
// We perfom a DOM read over a rendered item, this needs to happen after the first render or after the the column has changed
|
||||
this.optHeight = colEl.firstElementChild ? colEl.firstElementChild.clientHeight : 0;
|
||||
}
|
||||
this.refresh(forceRefresh, animated);
|
||||
}
|
||||
|
||||
render() {
|
||||
const col = this.col;
|
||||
const mode = getIonMode(this);
|
||||
return (
|
||||
<Host
|
||||
class={{
|
||||
[mode]: true,
|
||||
'picker-col': true,
|
||||
'picker-opts-left': this.col.align === 'left',
|
||||
'picker-opts-right': this.col.align === 'right',
|
||||
...getClassMap(col.cssClass),
|
||||
}}
|
||||
style={{
|
||||
'max-width': this.col.columnWidth,
|
||||
}}
|
||||
>
|
||||
{col.prefix && (
|
||||
<div class="picker-prefix" style={{ width: col.prefixWidth! }}>
|
||||
{col.prefix}
|
||||
</div>
|
||||
)}
|
||||
<div class="picker-opts" style={{ maxWidth: col.optionsWidth! }} ref={(el) => (this.optsEl = el)}>
|
||||
{col.options.map((o, index) => (
|
||||
<button
|
||||
aria-label={o.ariaLabel}
|
||||
class={{ 'picker-opt': true, 'picker-opt-disabled': !!o.disabled }}
|
||||
opt-index={index}
|
||||
>
|
||||
{o.text}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{col.suffix && (
|
||||
<div class="picker-suffix" style={{ width: col.suffixWidth! }}>
|
||||
{col.suffix}
|
||||
</div>
|
||||
)}
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const PICKER_OPT_SELECTED = 'picker-opt-selected';
|
||||
const DECELERATION_FRICTION = 0.97;
|
||||
const MAX_PICKER_SPEED = 90;
|
||||
const TRANSITION_DURATION = 150;
|
||||
@@ -24,11 +24,11 @@ describe('picker-column', () => {
|
||||
|
||||
const page = await newSpecPage({
|
||||
components: [PickerColumnCmp],
|
||||
template: () => <ion-picker-column col={col}></ion-picker-column>,
|
||||
template: () => <ion-picker-legacy-column col={col}></ion-picker-legacy-column>,
|
||||
});
|
||||
|
||||
const firstOption = page.body.querySelector('ion-picker-column .picker-opt:nth-child(1)')!;
|
||||
const secondOption = page.body.querySelector('ion-picker-column .picker-opt:nth-child(2)')!;
|
||||
const firstOption = page.body.querySelector('ion-picker-legacy-column .picker-opt:nth-child(1)')!;
|
||||
const secondOption = page.body.querySelector('ion-picker-legacy-column .picker-opt:nth-child(2)')!;
|
||||
|
||||
expect(firstOption.getAttribute('aria-label')).toBe('C Sharp');
|
||||
expect(secondOption.getAttribute('aria-label')).toBe(null);
|
||||
@@ -15,10 +15,12 @@ describe('picker-column: dynamic options', () => {
|
||||
|
||||
const page = await newSpecPage({
|
||||
components: [PickerColumnCmp],
|
||||
template: () => <ion-picker-column col={{ options: defaultOptions, name: 'animals' }}></ion-picker-column>,
|
||||
template: () => (
|
||||
<ion-picker-legacy-column col={{ options: defaultOptions, name: 'animals' }}></ion-picker-legacy-column>
|
||||
),
|
||||
});
|
||||
|
||||
const pickerCol = page.body.querySelector('ion-picker-column')!;
|
||||
const pickerCol = page.body.querySelector('ion-picker-legacy-column')!;
|
||||
|
||||
pickerCol.col = {
|
||||
options: [...defaultOptions, { text: 'Carrot', value: 'carrot' }],
|
||||
@@ -9,10 +9,10 @@ describe('picker-column', () => {
|
||||
|
||||
const page = await newSpecPage({
|
||||
components: [PickerColumnCmp],
|
||||
template: () => <ion-picker-column col={col}></ion-picker-column>,
|
||||
template: () => <ion-picker-legacy-column col={col}></ion-picker-legacy-column>,
|
||||
});
|
||||
|
||||
const pickerCol = page.body.querySelector('ion-picker-column')!;
|
||||
const pickerCol = page.body.querySelector('ion-picker-legacy-column')!;
|
||||
expect(pickerCol.classList.contains('test-class')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -4,13 +4,13 @@ import { configs, test } from '@utils/test/playwright';
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('picker-column'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(`/src/components/picker-column/test/standalone`, config);
|
||||
await page.goto(`/src/components/picker-legacy-column/test/standalone`, config);
|
||||
});
|
||||
|
||||
test('should present picker without ion-app', async ({ page }) => {
|
||||
const ionPickerDidPresent = await page.spyOnEvent('ionPickerDidPresent');
|
||||
|
||||
const picker = page.locator('ion-picker');
|
||||
const picker = page.locator('ion-picker-legacy');
|
||||
|
||||
await page.click('#single-column-button');
|
||||
|
||||
@@ -28,7 +28,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
const ionPickerDidPresent = await page.spyOnEvent('ionPickerDidPresent');
|
||||
const ionPickerColChangeEvent = await page.spyOnEvent('ionPickerColChange');
|
||||
|
||||
const column = page.locator('ion-picker-column');
|
||||
const column = page.locator('ion-picker-legacy-column');
|
||||
const secondOption = column.locator('.picker-opt').nth(1);
|
||||
|
||||
await page.click('#single-column-button');
|
||||
97
core/src/components/picker-legacy/picker.ios.scss
Normal file
@@ -0,0 +1,97 @@
|
||||
@import "./picker";
|
||||
@import "./picker.ios.vars";
|
||||
|
||||
// iOS Picker
|
||||
// --------------------------------------------------
|
||||
|
||||
:host {
|
||||
--background: #{$picker-ios-background-color};
|
||||
--border-width: #{1px 0 0};
|
||||
--border-color: #{$picker-ios-border-color};
|
||||
--height: #{$picker-ios-height};
|
||||
--backdrop-opacity: var(--ion-backdrop-opacity, 0.26);
|
||||
|
||||
color: $picker-ios-option-text-color;
|
||||
}
|
||||
|
||||
.picker-toolbar {
|
||||
display: flex;
|
||||
|
||||
height: $picker-ios-toolbar-height;
|
||||
|
||||
border-bottom: $hairlines-width solid var(--border-color);
|
||||
}
|
||||
|
||||
.picker-toolbar-button {
|
||||
flex: 1;
|
||||
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.picker-toolbar-button:last-child .picker-button {
|
||||
font-weight: $picker-ios-button-strong-font-weight;
|
||||
}
|
||||
|
||||
.picker-toolbar-button:first-child {
|
||||
font-weight: normal;
|
||||
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.picker-button,
|
||||
.picker-button.ion-activated {
|
||||
@include margin(0);
|
||||
@include padding($picker-ios-button-padding-top, $picker-ios-button-padding-end, $picker-ios-button-padding-bottom, $picker-ios-button-padding-start);
|
||||
|
||||
height: $picker-ios-button-height;
|
||||
|
||||
background: transparent;
|
||||
color: $picker-ios-button-text-color;
|
||||
|
||||
font-size: $picker-ios-button-font-size;
|
||||
}
|
||||
|
||||
.picker-columns {
|
||||
height: $picker-ios-height - $picker-ios-toolbar-height - 1;
|
||||
perspective: $picker-ios-column-perspective;
|
||||
}
|
||||
|
||||
.picker-above-highlight {
|
||||
@include position(0, null, null, 0);
|
||||
@include transform(translate3d(0, 0, 90px));
|
||||
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
||||
width: 100%;
|
||||
height: $picker-ios-option-offset-y + 4px;
|
||||
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
$picker-ios-top-background-color 20%,
|
||||
$picker-ios-bottom-background-color 100%);
|
||||
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.picker-below-highlight {
|
||||
@include position($picker-ios-option-offset-y + $picker-ios-option-height - 4, null, null, 0);
|
||||
@include transform(translate3d(0, 0, 90px));
|
||||
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
||||
width: 100%;
|
||||
height: $picker-ios-option-offset-y + $picker-ios-option-height;
|
||||
|
||||
border-top: 1px solid var(--border-color);
|
||||
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
$picker-ios-top-background-color 30%,
|
||||
$picker-ios-bottom-background-color 100%);
|
||||
|
||||
z-index: 11;
|
||||
}
|
||||
84
core/src/components/picker-legacy/picker.md.scss
Normal file
@@ -0,0 +1,84 @@
|
||||
@import "./picker";
|
||||
@import "./picker.md.vars";
|
||||
|
||||
// Material Design Picker
|
||||
// --------------------------------------------------
|
||||
|
||||
:host {
|
||||
--background: #{$picker-md-background-color};
|
||||
--border-width: #{$hairlines-width 0 0};
|
||||
--border-color: #{$picker-md-border-color};
|
||||
--height: #{$picker-md-height};
|
||||
--backdrop-opacity: var(--ion-backdrop-opacity, 0.26);
|
||||
|
||||
color: $picker-md-option-text-color;
|
||||
}
|
||||
|
||||
.picker-toolbar {
|
||||
display: flex;
|
||||
|
||||
justify-content: flex-end;
|
||||
|
||||
height: $picker-md-toolbar-height;
|
||||
}
|
||||
|
||||
.picker-button,
|
||||
.picker-button.ion-activated {
|
||||
@include margin(0);
|
||||
@include padding(0, 1.1em);
|
||||
|
||||
height: $picker-md-button-height;
|
||||
|
||||
background: $picker-md-button-background-color;
|
||||
color: $picker-md-button-text-color;
|
||||
|
||||
font-size: $picker-md-button-font-size;
|
||||
font-weight: 500;
|
||||
|
||||
text-transform: uppercase;
|
||||
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.picker-columns {
|
||||
height: $picker-md-height - $picker-md-toolbar-height;
|
||||
perspective: 1800px;
|
||||
}
|
||||
|
||||
.picker-above-highlight {
|
||||
@include position(0, null, null, 0);
|
||||
@include transform(translate3d(0, 0, 90px));
|
||||
|
||||
position: absolute;
|
||||
|
||||
width: 100%;
|
||||
height: $picker-md-option-offset-y + 4px;
|
||||
|
||||
border-bottom: 1px solid $picker-md-border-color;
|
||||
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
$picker-md-top-background-color 20%,
|
||||
$picker-md-bottom-background-color 100%);
|
||||
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.picker-below-highlight {
|
||||
@include position($picker-md-option-offset-y + $picker-md-option-height - 4, null, null, 0);
|
||||
@include transform(translate3d(0, 0, 90px));
|
||||
|
||||
position: absolute;
|
||||
|
||||
width: 100%;
|
||||
height: $picker-md-option-offset-y + $picker-md-option-height;
|
||||
|
||||
border-top: 1px solid $picker-md-border-color;
|
||||
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
$picker-md-top-background-color 30%,
|
||||
$picker-md-bottom-background-color 100%);
|
||||
|
||||
z-index: 11;
|
||||
}
|
||||
130
core/src/components/picker-legacy/picker.scss
Normal file
@@ -0,0 +1,130 @@
|
||||
@import "./picker.vars";
|
||||
|
||||
// Picker
|
||||
// --------------------------------------------------
|
||||
|
||||
:host {
|
||||
/**
|
||||
* @prop --background: Background of the picker
|
||||
* @prop --background-rgb: Background of the picker in rgb format
|
||||
*
|
||||
* @prop --border-radius: Border radius of the picker
|
||||
* @prop --border-color: Border color of the picker
|
||||
* @prop --border-width: Border width of the picker
|
||||
* @prop --border-style: Border style of the picker
|
||||
*
|
||||
* @prop --min-width: Minimum width of the picker
|
||||
* @prop --width: Width of the picker
|
||||
* @prop --max-width: Maximum width of the picker
|
||||
*
|
||||
* @prop --min-height: Minimum height of the picker
|
||||
* @prop --height: Height of the picker
|
||||
* @prop --max-height: Maximum height of the picker
|
||||
*
|
||||
* @prop --backdrop-opacity: Opacity of the backdrop
|
||||
*/
|
||||
--border-radius: 0;
|
||||
--border-style: solid;
|
||||
--min-width: auto;
|
||||
--width: #{$picker-width};
|
||||
--max-width: #{$picker-max-width};
|
||||
--min-height: auto;
|
||||
--max-height: auto;
|
||||
|
||||
@include font-smoothing();
|
||||
@include position(0, null, null, 0);
|
||||
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
outline: none;
|
||||
|
||||
font-family: $font-family-base;
|
||||
|
||||
contain: strict;
|
||||
user-select: none;
|
||||
z-index: $z-index-overlay;
|
||||
}
|
||||
|
||||
:host(.overlay-hidden) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.picker-wrapper {
|
||||
@include border-radius(var(--border-radius));
|
||||
@include position(null, 0, 0, 0);
|
||||
@include margin(auto);
|
||||
@include transform(translate3d(0, 100%, 0));
|
||||
|
||||
display: flex;
|
||||
position: absolute;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
width: var(--width);
|
||||
min-width: var(--min-width);
|
||||
max-width: var(--max-width);
|
||||
|
||||
height: var(--height);
|
||||
min-height: var(--min-height);
|
||||
max-height: var(--max-height);
|
||||
|
||||
border-width: var(--border-width);
|
||||
border-style: var(--border-style);
|
||||
border-color: var(--border-color);
|
||||
|
||||
background: var(--background);
|
||||
|
||||
contain: strict;
|
||||
overflow: hidden;
|
||||
z-index: $z-index-overlay-wrapper;
|
||||
}
|
||||
|
||||
// Picker Toolbar and Buttons
|
||||
// --------------------------------------------------
|
||||
|
||||
.picker-toolbar {
|
||||
width: 100%;
|
||||
|
||||
background: transparent;
|
||||
|
||||
contain: strict;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.picker-button {
|
||||
border: 0;
|
||||
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.picker-button {
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Picker Columns and Highlight
|
||||
// --------------------------------------------------
|
||||
|
||||
.picker-columns {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
margin-bottom: var(--ion-safe-area-bottom, 0);
|
||||
|
||||
contain: strict;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.picker-above-highlight,
|
||||
.picker-below-highlight {
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
404
core/src/components/picker-legacy/picker.tsx
Normal file
@@ -0,0 +1,404 @@
|
||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { Component, Element, Event, Host, Method, Prop, State, Watch, h } from '@stencil/core';
|
||||
import { raf } from '@utils/helpers';
|
||||
import { createLockController } from '@utils/lock-controller';
|
||||
import { printIonWarning } from '@utils/logging';
|
||||
import {
|
||||
createDelegateController,
|
||||
createTriggerController,
|
||||
BACKDROP,
|
||||
dismiss,
|
||||
eventMethod,
|
||||
isCancel,
|
||||
prepareOverlay,
|
||||
present,
|
||||
safeCall,
|
||||
setOverlayId,
|
||||
} from '@utils/overlays';
|
||||
import { getClassMap } from '@utils/theme';
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
import type { AnimationBuilder, CssClassMap, OverlayInterface, FrameworkDelegate } from '../../interface';
|
||||
import type { OverlayEventDetail } from '../../utils/overlays-interface';
|
||||
|
||||
import { iosEnterAnimation } from './animations/ios.enter';
|
||||
import { iosLeaveAnimation } from './animations/ios.leave';
|
||||
import type { PickerButton, PickerColumn } from './picker-interface';
|
||||
|
||||
// TODO(FW-2832): types
|
||||
|
||||
/**
|
||||
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
|
||||
*/
|
||||
@Component({
|
||||
tag: 'ion-picker-legacy',
|
||||
styleUrls: {
|
||||
ios: 'picker.ios.scss',
|
||||
md: 'picker.md.scss',
|
||||
},
|
||||
scoped: true,
|
||||
})
|
||||
export class Picker implements ComponentInterface, OverlayInterface {
|
||||
private readonly delegateController = createDelegateController(this);
|
||||
private readonly lockController = createLockController();
|
||||
private readonly triggerController = createTriggerController();
|
||||
|
||||
private durationTimeout?: ReturnType<typeof setTimeout>;
|
||||
lastFocus?: HTMLElement;
|
||||
|
||||
@Element() el!: HTMLIonPickerLegacyElement;
|
||||
|
||||
@State() presented = false;
|
||||
|
||||
/** @internal */
|
||||
@Prop() overlayIndex!: number;
|
||||
|
||||
/** @internal */
|
||||
@Prop() delegate?: FrameworkDelegate;
|
||||
|
||||
/** @internal */
|
||||
@Prop() hasController = false;
|
||||
|
||||
/**
|
||||
* If `true`, the keyboard will be automatically dismissed when the overlay is presented.
|
||||
*/
|
||||
@Prop() keyboardClose = true;
|
||||
|
||||
/**
|
||||
* Animation to use when the picker is presented.
|
||||
*/
|
||||
@Prop() enterAnimation?: AnimationBuilder;
|
||||
|
||||
/**
|
||||
* Animation to use when the picker is dismissed.
|
||||
*/
|
||||
@Prop() leaveAnimation?: AnimationBuilder;
|
||||
|
||||
/**
|
||||
* Array of buttons to be displayed at the top of the picker.
|
||||
*/
|
||||
@Prop() buttons: PickerButton[] = [];
|
||||
|
||||
/**
|
||||
* Array of columns to be displayed in the picker.
|
||||
*/
|
||||
@Prop() columns: PickerColumn[] = [];
|
||||
|
||||
/**
|
||||
* Additional classes to apply for custom CSS. If multiple classes are
|
||||
* provided they should be separated by spaces.
|
||||
*/
|
||||
@Prop() cssClass?: string | string[];
|
||||
|
||||
/**
|
||||
* Number of milliseconds to wait before dismissing the picker.
|
||||
*/
|
||||
@Prop() duration = 0;
|
||||
|
||||
/**
|
||||
* If `true`, a backdrop will be displayed behind the picker.
|
||||
*/
|
||||
@Prop() showBackdrop = true;
|
||||
|
||||
/**
|
||||
* If `true`, the picker will be dismissed when the backdrop is clicked.
|
||||
*/
|
||||
@Prop() backdropDismiss = true;
|
||||
|
||||
/**
|
||||
* If `true`, the picker will animate.
|
||||
*/
|
||||
@Prop() animated = true;
|
||||
|
||||
/**
|
||||
* Additional attributes to pass to the picker.
|
||||
*/
|
||||
@Prop() htmlAttributes?: { [key: string]: any };
|
||||
|
||||
/**
|
||||
* If `true`, the picker will open. If `false`, the picker will close.
|
||||
* Use this if you need finer grained control over presentation, otherwise
|
||||
* just use the pickerController or the `trigger` property.
|
||||
* Note: `isOpen` will not automatically be set back to `false` when
|
||||
* the picker dismisses. You will need to do that in your code.
|
||||
*/
|
||||
@Prop() isOpen = false;
|
||||
@Watch('isOpen')
|
||||
onIsOpenChange(newValue: boolean, oldValue: boolean) {
|
||||
if (newValue === true && oldValue === false) {
|
||||
this.present();
|
||||
} else if (newValue === false && oldValue === true) {
|
||||
this.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An ID corresponding to the trigger element that
|
||||
* causes the picker to open when clicked.
|
||||
*/
|
||||
@Prop() trigger: string | undefined;
|
||||
@Watch('trigger')
|
||||
triggerChanged() {
|
||||
const { trigger, el, triggerController } = this;
|
||||
if (trigger) {
|
||||
triggerController.addClickListener(el, trigger);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted after the picker has presented.
|
||||
*/
|
||||
@Event({ eventName: 'ionPickerDidPresent' }) didPresent!: EventEmitter<void>;
|
||||
|
||||
/**
|
||||
* Emitted before the picker has presented.
|
||||
*/
|
||||
@Event({ eventName: 'ionPickerWillPresent' }) willPresent!: EventEmitter<void>;
|
||||
|
||||
/**
|
||||
* Emitted before the picker has dismissed.
|
||||
*/
|
||||
@Event({ eventName: 'ionPickerWillDismiss' }) willDismiss!: EventEmitter<OverlayEventDetail>;
|
||||
|
||||
/**
|
||||
* Emitted after the picker has dismissed.
|
||||
*/
|
||||
@Event({ eventName: 'ionPickerDidDismiss' }) didDismiss!: EventEmitter<OverlayEventDetail>;
|
||||
|
||||
/**
|
||||
* Emitted after the picker has presented.
|
||||
* Shorthand for ionPickerWillDismiss.
|
||||
*/
|
||||
@Event({ eventName: 'didPresent' }) didPresentShorthand!: EventEmitter<void>;
|
||||
|
||||
/**
|
||||
* Emitted before the picker has presented.
|
||||
* Shorthand for ionPickerWillPresent.
|
||||
*/
|
||||
@Event({ eventName: 'willPresent' }) willPresentShorthand!: EventEmitter<void>;
|
||||
|
||||
/**
|
||||
* Emitted before the picker has dismissed.
|
||||
* Shorthand for ionPickerWillDismiss.
|
||||
*/
|
||||
@Event({ eventName: 'willDismiss' }) willDismissShorthand!: EventEmitter<OverlayEventDetail>;
|
||||
|
||||
/**
|
||||
* Emitted after the picker has dismissed.
|
||||
* Shorthand for ionPickerDidDismiss.
|
||||
*/
|
||||
@Event({ eventName: 'didDismiss' }) didDismissShorthand!: EventEmitter<OverlayEventDetail>;
|
||||
|
||||
connectedCallback() {
|
||||
prepareOverlay(this.el);
|
||||
this.triggerChanged();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.triggerController.removeClickListener();
|
||||
}
|
||||
|
||||
componentWillLoad() {
|
||||
setOverlayId(this.el);
|
||||
}
|
||||
|
||||
componentDidLoad() {
|
||||
printIonWarning(
|
||||
'ion-picker-legacy and ion-picker-legacy-column have been deprecated in favor of new versions of the ion-picker and ion-picker-column components. These new components display inline with your page content allowing for more presentation flexibility than before.',
|
||||
this.el
|
||||
);
|
||||
|
||||
/**
|
||||
* If picker was rendered with isOpen="true"
|
||||
* then we should open picker immediately.
|
||||
*/
|
||||
if (this.isOpen === true) {
|
||||
raf(() => this.present());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Present the picker overlay after it has been created.
|
||||
*/
|
||||
@Method()
|
||||
async present(): Promise<void> {
|
||||
const unlock = await this.lockController.lock();
|
||||
|
||||
await this.delegateController.attachViewToDom();
|
||||
|
||||
await present(this, 'pickerEnter', iosEnterAnimation, iosEnterAnimation, undefined);
|
||||
|
||||
if (this.duration > 0) {
|
||||
this.durationTimeout = setTimeout(() => this.dismiss(), this.duration);
|
||||
}
|
||||
|
||||
unlock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss the picker overlay after it has been presented.
|
||||
*
|
||||
* @param data Any data to emit in the dismiss events.
|
||||
* @param role The role of the element that is dismissing the picker.
|
||||
* This can be useful in a button handler for determining which button was
|
||||
* clicked to dismiss the picker.
|
||||
* Some examples include: ``"cancel"`, `"destructive"`, "selected"`, and `"backdrop"`.
|
||||
*/
|
||||
@Method()
|
||||
async dismiss(data?: any, role?: string): Promise<boolean> {
|
||||
const unlock = await this.lockController.lock();
|
||||
|
||||
if (this.durationTimeout) {
|
||||
clearTimeout(this.durationTimeout);
|
||||
}
|
||||
const dismissed = await dismiss(this, data, role, 'pickerLeave', iosLeaveAnimation, iosLeaveAnimation);
|
||||
|
||||
if (dismissed) {
|
||||
this.delegateController.removeViewFromDom();
|
||||
}
|
||||
|
||||
unlock();
|
||||
|
||||
return dismissed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves when the picker did dismiss.
|
||||
*/
|
||||
@Method()
|
||||
onDidDismiss<T = any>(): Promise<OverlayEventDetail<T>> {
|
||||
return eventMethod(this.el, 'ionPickerDidDismiss');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves when the picker will dismiss.
|
||||
*/
|
||||
@Method()
|
||||
onWillDismiss<T = any>(): Promise<OverlayEventDetail<T>> {
|
||||
return eventMethod(this.el, 'ionPickerWillDismiss');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the column that matches the specified name.
|
||||
*
|
||||
* @param name The name of the column.
|
||||
*/
|
||||
@Method()
|
||||
getColumn(name: string): Promise<PickerColumn | undefined> {
|
||||
return Promise.resolve(this.columns.find((column) => column.name === name));
|
||||
}
|
||||
|
||||
private async buttonClick(button: PickerButton) {
|
||||
const role = button.role;
|
||||
if (isCancel(role)) {
|
||||
return this.dismiss(undefined, role);
|
||||
}
|
||||
const shouldDismiss = await this.callButtonHandler(button);
|
||||
if (shouldDismiss) {
|
||||
return this.dismiss(this.getSelected(), button.role);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private async callButtonHandler(button: PickerButton | undefined) {
|
||||
if (button) {
|
||||
// a handler has been provided, execute it
|
||||
// pass the handler the values from the inputs
|
||||
const rtn = await safeCall(button.handler, this.getSelected());
|
||||
if (rtn === false) {
|
||||
// if the return value of the handler is false then do not dismiss
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private getSelected() {
|
||||
const selected: { [k: string]: any } = {};
|
||||
this.columns.forEach((col, index) => {
|
||||
const selectedColumn = col.selectedIndex !== undefined ? col.options[col.selectedIndex] : undefined;
|
||||
selected[col.name] = {
|
||||
text: selectedColumn ? selectedColumn.text : undefined,
|
||||
value: selectedColumn ? selectedColumn.value : undefined,
|
||||
columnIndex: index,
|
||||
};
|
||||
});
|
||||
return selected;
|
||||
}
|
||||
|
||||
private onBackdropTap = () => {
|
||||
this.dismiss(undefined, BACKDROP);
|
||||
};
|
||||
|
||||
private dispatchCancelHandler = (ev: CustomEvent) => {
|
||||
const role = ev.detail.role;
|
||||
if (isCancel(role)) {
|
||||
const cancelButton = this.buttons.find((b) => b.role === 'cancel');
|
||||
this.callButtonHandler(cancelButton);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { htmlAttributes } = this;
|
||||
const mode = getIonMode(this);
|
||||
return (
|
||||
<Host
|
||||
aria-modal="true"
|
||||
tabindex="-1"
|
||||
{...(htmlAttributes as any)}
|
||||
style={{
|
||||
zIndex: `${20000 + this.overlayIndex}`,
|
||||
}}
|
||||
class={{
|
||||
[mode]: true,
|
||||
|
||||
// Used internally for styling
|
||||
[`picker-${mode}`]: true,
|
||||
'overlay-hidden': true,
|
||||
...getClassMap(this.cssClass),
|
||||
}}
|
||||
onIonBackdropTap={this.onBackdropTap}
|
||||
onIonPickerWillDismiss={this.dispatchCancelHandler}
|
||||
>
|
||||
<ion-backdrop visible={this.showBackdrop} tappable={this.backdropDismiss}></ion-backdrop>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
|
||||
<div class="picker-wrapper ion-overlay-wrapper" role="dialog">
|
||||
<div class="picker-toolbar">
|
||||
{this.buttons.map((b) => (
|
||||
<div class={buttonWrapperClass(b)}>
|
||||
<button type="button" onClick={() => this.buttonClick(b)} class={buttonClass(b)}>
|
||||
{b.text}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="picker-columns">
|
||||
<div class="picker-above-highlight"></div>
|
||||
{this.presented && this.columns.map((c) => <ion-picker-legacy-column col={c}></ion-picker-legacy-column>)}
|
||||
<div class="picker-below-highlight"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div tabindex="0"></div>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const buttonWrapperClass = (button: PickerButton): CssClassMap => {
|
||||
return {
|
||||
[`picker-toolbar-${button.role}`]: button.role !== undefined,
|
||||
'picker-toolbar-button': true,
|
||||
};
|
||||
};
|
||||
|
||||
const buttonClass = (button: PickerButton): CssClassMap => {
|
||||
return {
|
||||
'picker-button': true,
|
||||
'ion-activatable': true,
|
||||
...getClassMap(button.cssClass),
|
||||
};
|
||||
};
|
||||
10
core/src/components/picker-legacy/picker.vars.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
@import "../../themes/ionic.globals";
|
||||
|
||||
// Picker
|
||||
// --------------------------------------------------
|
||||
|
||||
/// @prop - Width of the picker
|
||||
$picker-width: 100% !default;
|
||||
|
||||
/// @prop - Max width of the picker
|
||||
$picker-max-width: 500px !default;
|
||||