Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
629d862c54 | ||
|
|
2c773ed0e6 | ||
|
|
b1fc67227c | ||
|
|
75ee951ce8 | ||
|
|
2f3f9dc9ca | ||
|
|
b68c93d55d | ||
|
|
eace6425a2 | ||
|
|
1aeb19403b | ||
|
|
9d0834b201 | ||
|
|
7b21bd40a6 | ||
|
|
0b469646b2 | ||
|
|
5e47412e1f | ||
|
|
cc45e2220b | ||
|
|
7ac0018a3c | ||
|
|
a7c966776a | ||
|
|
7de4e34f13 | ||
|
|
4b5e62e60f | ||
|
|
9883eac0f7 | ||
|
|
aa2a7f5271 | ||
|
|
2509d565b2 | ||
|
|
ce89057641 | ||
|
|
5aafd68f03 | ||
|
|
098ed054b1 | ||
|
|
7ba939fb94 | ||
|
|
6cd819a059 | ||
|
|
9bcee94e0b | ||
|
|
409df1bea5 | ||
|
|
021712bd7d |
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
|
||||
|
||||
339
BREAKING.md
@@ -4,338 +4,65 @@ This is a comprehensive list of the breaking changes introduced in the major ver
|
||||
|
||||
## Versions
|
||||
|
||||
- [Version 7.x](#version-7x)
|
||||
- [Version 8.x](#version-8x)
|
||||
- [Version 7.x](./BREAKING_ARCHIVE/v7.md)
|
||||
- [Version 6.x](./BREAKING_ARCHIVE/v6.md)
|
||||
- [Version 5.x](./BREAKING_ARCHIVE/v5.md)
|
||||
- [Version 4.x](./BREAKING_ARCHIVE/v4.md)
|
||||
- [Legacy](https://github.com/ionic-team/ionic-v3/blob/master/CHANGELOG.md)
|
||||
|
||||
## Version 7.x
|
||||
## Version 8.x
|
||||
|
||||
- [Browser and Platform Support](#version-7x-browser-platform-support)
|
||||
- [Components](#version-7x-components)
|
||||
- [Accordion Group](#version-7x-accordion-group)
|
||||
- [Action Sheet](#version-7x-action-sheet)
|
||||
- [Back Button](#version-7x-back-button)
|
||||
- [Button](#version-7x-button)
|
||||
- [Card Header](#version-7x-card-header)
|
||||
- [Checkbox](#version-7x-checkbox)
|
||||
- [Datetime](#version-7x-datetime)
|
||||
- [Input](#version-7x-input)
|
||||
- [Item](#version-7x-item)
|
||||
- [Modal](#version-7x-modal)
|
||||
- [Overlays](#version-7x-overlays)
|
||||
- [Picker](#version-7x-picker)
|
||||
- [Radio Group](#version-7x-radio-group)
|
||||
- [Range](#version-7x-range)
|
||||
- [Searchbar](#version-7x-searchbar)
|
||||
- [Segment](#version-7x-segment)
|
||||
- [Select](#version-7x-select)
|
||||
- [Slides](#version-7x-slides)
|
||||
- [Textarea](#version-7x-textarea)
|
||||
- [Toggle](#version-7x-toggle)
|
||||
- [Virtual Scroll](#version-7x-virtual-scroll)
|
||||
- [Config](#version-7x-config)
|
||||
- [Types](#version-7x-types)
|
||||
- [Overlay Attribute Interfaces](#version-7x-overlay-attribute-interfaces)
|
||||
- [JavaScript Frameworks](#version-7x-javascript-frameworks)
|
||||
- [Angular](#version-7x-angular)
|
||||
- [React](#version-7x-react)
|
||||
- [Vue](#version-7x-vue)
|
||||
- [CSS Utilities](#version-7x-css-utilities)
|
||||
- [hidden attribute](#version-7x-hidden-attribute)
|
||||
- [Browser and Platform Support](#version-8x-browser-platform-support)
|
||||
- [Components](#version-8x-components)
|
||||
- [Content](#version-8x-content)
|
||||
- [Datetime](#version-8x-datetime)
|
||||
- [Picker](#version-8x-picker)
|
||||
|
||||
<h2 id="version-7x-browser-platform-support">Browser and Platform Support</h2>
|
||||
<h2 id="version-8x-browser-platform-support">Browser and Platform Support</h2>
|
||||
|
||||
This section details the desktop browser, JavaScript framework, and mobile platform versions that are supported by Ionic 7.
|
||||
This section details the desktop browser, JavaScript framework, and mobile platform versions that are supported by Ionic 8.
|
||||
|
||||
**Minimum Browser Versions**
|
||||
| Desktop Browser | Supported Versions |
|
||||
| --------------- | ----------------- |
|
||||
| Chrome | 79+ |
|
||||
| Safari | 14+ |
|
||||
| Firefox | 70+ |
|
||||
| Edge | 79+ |
|
||||
| Chrome | 89+ |
|
||||
| Safari | 15+ |
|
||||
| Firefox | 75+ |
|
||||
| Edge | 89+ |
|
||||
|
||||
**Minimum JavaScript Framework Versions**
|
||||
|
||||
| Framework | Supported Version |
|
||||
| --------- | --------------------- |
|
||||
| Angular | 14+ |
|
||||
| Angular | 16+ |
|
||||
| React | 17+ |
|
||||
| Vue | 3.0.6+ |
|
||||
|
||||
**Minimum Mobile Platform Versions**
|
||||
|
||||
| Platform | Supported Version |
|
||||
| -------- | ---------------------- |
|
||||
| iOS | 14+ |
|
||||
| Android | 5.1+ with Chromium 79+ |
|
||||
| iOS | 15+ |
|
||||
| Android | 5.1+ with Chromium 89+ |
|
||||
|
||||
<h2 id="version-7x-components">Components</h2>
|
||||
<h2 id="version-8x-components">Components</h2>
|
||||
|
||||
<h4 id="version-7x-accordion-group">Accordion Group</h4>
|
||||
<h4 id="version-8x-content">Content</h4>
|
||||
|
||||
- `ionChange` is no longer emitted when the `value` of `ion-accordion-group` is modified externally. `ionChange` is only emitted from user committed changes, such as clicking or tapping the accordion header.
|
||||
- Content no longer sets the `--background` custom property when the `.outer-content` class is set on the host.
|
||||
|
||||
- Accordion Group no longer automatically adjusts the `value` property when passed an array and `multiple="false"`. Developers should update their apps to ensure they are using the API correctly.
|
||||
<h4 id="version-8x-datetime">Datetime</h4>
|
||||
|
||||
<h4 id="version-7x-action-sheet">Action Sheet</h4>
|
||||
- The CSS shadow part for `month-year-button` has been changed to target a `button` element instead of `ion-item`. Developers should verify their UI renders as expected for the month/year toggle button inside of `ion-datetime`.
|
||||
- Developers using the CSS variables available on `ion-item` will need to migrate their CSS to use CSS properties. For example:
|
||||
```diff
|
||||
ion-datetime::part(month-year-button) {
|
||||
- --background: red;
|
||||
|
||||
- Action Sheet is updated to align with the design specification.
|
||||
+ background: red;
|
||||
}
|
||||
```
|
||||
<h2 id="version-8x-picker">Picker</h2>
|
||||
|
||||
**Design tokens**
|
||||
|
||||
| Token | Previous Value | New Value |
|
||||
| ---------- | -------------- | --------- |
|
||||
| `--height` | `100%` | `auto` |
|
||||
|
||||
<h4 id="version-7x-button">Button</h4>
|
||||
|
||||
- Button is updated to align with the design specification for iOS.
|
||||
|
||||
**Design tokens**
|
||||
|
||||
| Token | Previous Value | New Value |
|
||||
| ---------------------------------- | -------------- | --------- |
|
||||
| `$button-ios-letter-spacing` | `-0.03em` | `0` |
|
||||
| `$button-ios-clear-letter-spacing` | `0` | Removed |
|
||||
| `$button-ios-height` | `2.8em` | `3.1em` |
|
||||
| `$button-ios-border-radius` | `10px` | `14px` |
|
||||
| `$button-ios-large-height` | `2.8em` | `3.1em` |
|
||||
| `$button-ios-large-border-radius` | `12px` | `16px` |
|
||||
|
||||
<h4 id="version-7x-back-button">Back Button</h4>
|
||||
|
||||
- Back Button is updated to align with the design specification for iOS.
|
||||
|
||||
**Design tokens**
|
||||
|
||||
| Token | Previous Value | New Value |
|
||||
| ------------------- | -------------- | --------- |
|
||||
| `--icon-margin-end` | `-5px` | `1px` |
|
||||
| `--icon-font-size` | `1.85em` | `1.6em` |
|
||||
|
||||
<h4 id="version-7x-card-header">Card Header</h4>
|
||||
|
||||
- The card header has ben changed to a flex container with direction set to `column` (top to bottom). In `ios` mode the direction is set to `column-reverse` which results in the subtitle displaying on top of the title.
|
||||
|
||||
<h4 id="version-7x-checkbox">Checkbox</h4>
|
||||
|
||||
- `ionChange` is no longer emitted when the `checked` property of `ion-checkbox` is modified externally. `ionChange` is only emitted from user committed changes, such as clicking or tapping the checkbox.
|
||||
|
||||
- The `--background` and `--background-checked` CSS variables have been renamed to `--checkbox-background` and `--checkbox-background-checked` respectively.
|
||||
|
||||
<h4 id="version-7x-datetime">Datetime</h4>
|
||||
|
||||
- `ionChange` is no longer emitted when the `value` property of `ion-datetime` is modified externally. `ionChange` is only emitted from user committed changes, such as clicking or tapping a date.
|
||||
|
||||
- Datetime no longer automatically adjusts the `value` property when passed an array and `multiple="false"`. Developers should update their apps to ensure they are using the API correctly.
|
||||
|
||||
- Datetime no longer incorrectly reports the time zone when `value` is updated. Datetime does not manage time zones, so any time zone information provided is ignored.
|
||||
|
||||
- Passing the empty string to the `value` property will now error as it is not a valid ISO-8601 value.
|
||||
|
||||
- The haptics when swiping the wheel picker are now enabled only on iOS.
|
||||
|
||||
<h4 id="version-7x-input">Input</h4>
|
||||
|
||||
- `ionChange` is no longer emitted when the `value` of `ion-input` is modified externally. `ionChange` is only emitted from user committed changes, such as typing in the input and the input losing focus, clicking the clear action within the input, or pressing the "Enter" key.
|
||||
|
||||
- If your application requires immediate feedback based on the user typing actively in the input, consider migrating your event listeners to using `ionInput` instead.
|
||||
|
||||
- The `debounce` property has been updated to control the timing in milliseconds to delay the event emission of the `ionInput` event after each keystroke. Previously it would delay the event emission of `ionChange`.
|
||||
|
||||
- The `debounce` property's default value has changed from `0` to `undefined`. If `debounce` is undefined, the `ionInput` event will fire immediately.
|
||||
|
||||
- The `detail` payload for the `ionInput` event now contains an object with the current `value` as well as the native event that triggered `ionInput`.
|
||||
|
||||
**Design tokens**
|
||||
|
||||
| Token | Previous Value | New Value |
|
||||
| ----------------------- | -------------- | --------- |
|
||||
| `--placeholder-opacity` | `0.5` | `0.6` |
|
||||
|
||||
<h4 id="version-7x-item">Item</h4>
|
||||
|
||||
**Design tokens**
|
||||
|
||||
iOS:
|
||||
|
||||
| Token | Previous Value | New Value |
|
||||
| --------------------- | -------------- | --------- |
|
||||
| `$item-ios-font-size` | `17px` | `16px` |
|
||||
| `--inner-padding-end` | `10px` | `16px` |
|
||||
| `--padding-start` | `20px` | `16px` |
|
||||
|
||||
<h4 id="version-7x-modal">Modal</h4>
|
||||
|
||||
- The `swipeToClose` property has been removed in favor of `canDismiss`.
|
||||
- The `canDismiss` property now defaults to `true` and can no longer be set to `undefined`.
|
||||
|
||||
<h4 id="version-7x-overlays">Overlays</h4>
|
||||
|
||||
Ionic now listens on the `keydown` event instead of the `keyup` event when determining when to dismiss overlays via the "Escape" key. Any applications that were listening on `keyup` to suppress this behavior should listen on `keydown` instead.
|
||||
|
||||
<h4 id="version-7x-picker">Picker</h4>
|
||||
|
||||
- The `refresh` key has been removed from the `PickerColumn` interface. Developers should use the `columns` property to refresh the `ion-picker` view.
|
||||
|
||||
<h4 id="version-7x-radio-group">Radio Group</h4>
|
||||
|
||||
- `ionChange` is no longer emitted when the `value` of `ion-radio-group` is modified externally. `ionChange` is only emitted from user committed changes, such as clicking or tapping an `ion-radio` in the group.
|
||||
|
||||
<h4 id="version-7x-range">Range</h4>
|
||||
|
||||
- Range is updated to align with the design specification for supported modes.
|
||||
|
||||
**Design tokens**
|
||||
|
||||
iOS:
|
||||
|
||||
| Token | Previous Value | New Value |
|
||||
| --------------------------------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| `--bar-border-radius` | `0px` | `$range-ios-bar-border-radius` (`2px` default) |
|
||||
| `--knob-size` | `28px` | `$range-ios-knob-width` (`26px` default) |
|
||||
| `$range-ios-bar-height` | `2px` | `4px` |
|
||||
| `$range-ios-bar-background-color` | `rgba(var(--ion-text-color-rgb, 0, 0, 0), .1)` | `var(--ion-color-step-900, #e6e6e6)` |
|
||||
| `$range-ios-knob-box-shadow` | `0 3px 1px rgba(0, 0, 0, .1), 0 4px 8px rgba(0, 0, 0, .13), 0 0 0 1px rgba(0, 0, 0, .02)` | `0px 0.5px 4px rgba(0, 0, 0, 0.12), 0px 6px 13px rgba(0, 0, 0, 0.12)` |
|
||||
| `$range-ios-knob-width` | `28px` | `26px` |
|
||||
|
||||
- `ionChange` is no longer emitted when the `value` of `ion-range` is modified externally. `ionChange` is only emitted from user committed changes, such as dragging and releasing the range knob or selecting a new value with the keyboard arrows.
|
||||
- If your application requires immediate feedback based on the user actively dragging the range knob, consider migrating your event listeners to using `ionInput` instead.
|
||||
|
||||
- The `debounce` property's value value has changed from `0` to `undefined`. If `debounce` is undefined, the `ionInput` event will fire immediately.
|
||||
|
||||
- Range no longer clamps assigned values within bounds. Developers will need to validate the value they are assigning to `ion-range` is within the `min` and `max` bounds when programmatically assigning a value.
|
||||
|
||||
- The `name` property defaults to `ion-r-${rangeIds++}` where `rangeIds` is a number that is incremented for every instance of `ion-range`.
|
||||
|
||||
<h4 id="version-7x-searchbar">Searchbar</h4>
|
||||
|
||||
- `ionChange` is no longer emitted when the `value` of `ion-searchbar` is modified externally. `ionChange` is only emitted from user committed changes, such as typing in the searchbar and the searchbar losing focus or pressing the "Enter" key.
|
||||
|
||||
- If your application requires immediate feedback based on the user typing actively in the searchbar, consider migrating your event listeners to using `ionInput` instead.
|
||||
|
||||
- The `debounce` property has been updated to control the timing in milliseconds to delay the event emission of the `ionInput` event after each keystroke. Previously it would delay the event emission of `ionChange`.
|
||||
|
||||
- The `debounce` property's default value has changed from 250 to `undefined`. If `debounce` is undefined, the `ionInput` event will fire immediately.
|
||||
|
||||
- The `detail` payload for the `ionInput` event now contains an object with the current `value` as well as the native event that triggered `ionInput`.
|
||||
|
||||
**Design tokens**
|
||||
|
||||
| Token | Previous Value | New Value |
|
||||
| ----------------------- | -------------- | --------- |
|
||||
| `--placeholder-opacity` | `0.5` | `0.6` |
|
||||
|
||||
|
||||
<h4 id="version-7x-segment">Segment</h4>
|
||||
|
||||
- `ionChange` is no longer emitted when the `value` of `ion-segment` is modified externally. `ionChange` is only emitted from user committed changes, such as clicking a segment button or dragging to activate a segment button.
|
||||
|
||||
- The type signature of `value` supports `string | undefined`. Previously the type signature was `string | null | undefined`.
|
||||
- Developers needing to clear the checked segment item should assign a value of `''` instead of `null`.
|
||||
|
||||
<h4 id="version-7x-select">Select</h4>
|
||||
|
||||
- `ionChange` is no longer emitted when the `value` of `ion-select` is modified externally. `ionChange` is only emitted from user committed changes, such as confirming a selected option in the select's overlay.
|
||||
|
||||
- The `icon` CSS Shadow Part now targets an `ion-icon` component.
|
||||
|
||||
**Design tokens**
|
||||
|
||||
| Token | Previous Value | New Value |
|
||||
| ----------------------- | -------------- | --------- |
|
||||
| `--placeholder-opacity` | `0.33` | `0.6` |
|
||||
|
||||
<h4 id="version-7x-slides">Slides</h4>
|
||||
|
||||
`ion-slides`, `ion-slide`, and the `IonicSwiper` plugin have been removed from Ionic.
|
||||
|
||||
Developers using these components will need to migrate to using Swiper.js directly, optionally using the `IonicSlides` plugin. Guides for migration and usage are linked below:
|
||||
|
||||
- [Angular](https://ionicframework.com/docs/angular/slides)
|
||||
- [React](https://ionicframework.com/docs/react/slides)
|
||||
- [Vue](https://ionicframework.com/docs/vue/slides)
|
||||
|
||||
<h4 id="version-7x-textarea">Textarea</h4>
|
||||
|
||||
- `ionChange` is no longer emitted when the `value` of `ion-textarea` is modified externally. `ionChange` is only emitted from user committed changes, such as typing in the textarea and the textarea losing focus.
|
||||
|
||||
- If your application requires immediate feedback based on the user typing actively in the textarea, consider migrating your event listeners to using `ionInput` instead.
|
||||
|
||||
- The `debounce` property has been updated to control the timing in milliseconds to delay the event emission of the `ionInput` event after each keystroke. Previously it would delay the event emission of `ionChange`.
|
||||
|
||||
- The `debounce` property's default value has changed from `0` to `undefined`. If `debounce` is undefined, the `ionInput` event will fire immediately.
|
||||
|
||||
- `ionInput` dispatches an event detail of `null` when the textarea is cleared as a result of `clear-on-edit="true"`.
|
||||
|
||||
- The `detail` payload for the `ionInput` event now contains an object with the current `value` as well as the native event that triggered `ionInput`.
|
||||
|
||||
**Design tokens**
|
||||
|
||||
| Token | Previous Value | New Value |
|
||||
| ----------------------- | -------------- | --------- |
|
||||
| `--placeholder-opacity` | `0.5` | `0.6` |
|
||||
|
||||
|
||||
<h4 id="version-7x-toggle">Toggle</h4>
|
||||
|
||||
- `ionChange` is no longer emitted when the `checked` property of `ion-toggle` is modified externally. `ionChange` is only emitted from user committed changes, such as clicking the toggle to set it on or off.
|
||||
|
||||
- The `--background` and `--background-checked` variables have been renamed to `--track-background` and `--track-background-checked`, respectively.
|
||||
|
||||
<h4 id="version-7x-virtual-scroll">Virtual Scroll</h4>
|
||||
|
||||
`ion-virtual-scroll` has been removed from Ionic.
|
||||
|
||||
Developers using the component will need to migrate to a virtual scroll solution provided by their framework:
|
||||
|
||||
- [Angular](https://ionicframework.com/docs/angular/virtual-scroll)
|
||||
- [React](https://ionicframework.com/docs/react/virtual-scroll)
|
||||
- [Vue](https://ionicframework.com/docs/vue/virtual-scroll)
|
||||
|
||||
Any references to the virtual scroll types from `@ionic/core` have been removed. Please remove or replace these types: `Cell`, `VirtualNode`, `CellType`, `NodeChange`, `HeaderFn`, `ItemHeightFn`, `FooterHeightFn`, `ItemRenderFn` and `DomRenderFn`.
|
||||
|
||||
<h2 id="version-7x-config">Config</h2>
|
||||
|
||||
- `innerHTMLTemplatesEnabled` defaults to `false`. Developers who wish to use the `innerHTML` functionality inside of `ion-alert`, `ion-infinite-scroll-content`, `ion-loading`, `ion-refresher-content`, and `ion-toast` must set this config to `true` and properly sanitize their content.
|
||||
|
||||
<h2 id="version-7x-types">Types</h2>
|
||||
|
||||
<h4 id="version-7x-overlay-attribute-interfaces">Overlay Attribute Interfaces</h4>
|
||||
|
||||
`ActionSheetAttributes`, `AlertAttributes`, `AlertTextareaAttributes`, `AlertInputAttributes`, `LoadingAttributes`, `ModalAttributes`, `PickerAttributes`, `PopoverAttributes`, and `ToastAttributes` have been removed. Developers should use `{ [key: string]: any }` instead.
|
||||
|
||||
<h2 id="version-7x-javascript-frameworks">JavaScript Frameworks</h2>
|
||||
|
||||
<h4 id="version-7x-angular">Angular</h4>
|
||||
|
||||
- Angular v14 is now required to use `@ionic/angular` and `@ionic/angular-server`. Upgrade your project to Angular v14 by following the [Angular v14 update guide](https://update.angular.io/?l=3&v=13.0-14.0).
|
||||
|
||||
- `null` values on form components will no longer be converted to the empty string (`''`) or `false`. This impacts `ion-checkbox`, `ion-datetime`, `ion-input`, `ion-radio`, `ion-radio-group`, `ion-range`, `ion-searchbar`, `ion-segment`, `ion-select`, `ion-textarea`, and `ion-toggle`.
|
||||
|
||||
- The dev-preview `environmentInjector` property has been removed from `ion-tabs` and `ion-router-outlet`. Standalone component routing is now available without additional custom configuration. Remove the `environmentInjector` property from your `ion-tabs` and `ion-router-outlet` components.
|
||||
|
||||
<h4 id="version-7x-react">React</h4>
|
||||
|
||||
`@ionic/react` and `@ionic/react-router` no longer ship a CommonJS entry point. Instead, only an ES Module entry point is provided for improved compatibility with Vite.
|
||||
|
||||
<h4 id="version-7x-vue">Vue</h4>
|
||||
|
||||
`@ionic/vue` and `@ionic/vue-router` no longer ship a CommonJS entry point. Instead, only an ES Module entry point is provided for improved compatibility with Vite.
|
||||
|
||||
<h2 id="version-7x-css-utilities">CSS Utilities</h2>
|
||||
|
||||
<h4 id="version-7x-hidden-attribute">`hidden` attribute</h4>
|
||||
|
||||
The `[hidden]` attribute has been removed from Ionic's global stylesheet. The `[hidden]` attribute can continue to be used, but developers will get the [native `hidden` implementation](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/hidden) instead. The main difference is that the native implementation is easier to override using `display` than Ionic's implementation.
|
||||
|
||||
Developers can add the following CSS to their global stylesheet if they need the old behavior:
|
||||
|
||||
```css
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
```
|
||||
- `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.
|
||||
331
BREAKING_ARCHIVE/v7.md
Normal file
@@ -0,0 +1,331 @@
|
||||
# Breaking Changes
|
||||
|
||||
## Version 7.x
|
||||
|
||||
- [Browser and Platform Support](#version-7x-browser-platform-support)
|
||||
- [Components](#version-7x-components)
|
||||
- [Accordion Group](#version-7x-accordion-group)
|
||||
- [Action Sheet](#version-7x-action-sheet)
|
||||
- [Back Button](#version-7x-back-button)
|
||||
- [Button](#version-7x-button)
|
||||
- [Card Header](#version-7x-card-header)
|
||||
- [Checkbox](#version-7x-checkbox)
|
||||
- [Datetime](#version-7x-datetime)
|
||||
- [Input](#version-7x-input)
|
||||
- [Item](#version-7x-item)
|
||||
- [Modal](#version-7x-modal)
|
||||
- [Overlays](#version-7x-overlays)
|
||||
- [Picker](#version-7x-picker)
|
||||
- [Radio Group](#version-7x-radio-group)
|
||||
- [Range](#version-7x-range)
|
||||
- [Searchbar](#version-7x-searchbar)
|
||||
- [Segment](#version-7x-segment)
|
||||
- [Select](#version-7x-select)
|
||||
- [Slides](#version-7x-slides)
|
||||
- [Textarea](#version-7x-textarea)
|
||||
- [Toggle](#version-7x-toggle)
|
||||
- [Virtual Scroll](#version-7x-virtual-scroll)
|
||||
- [Config](#version-7x-config)
|
||||
- [Types](#version-7x-types)
|
||||
- [Overlay Attribute Interfaces](#version-7x-overlay-attribute-interfaces)
|
||||
- [JavaScript Frameworks](#version-7x-javascript-frameworks)
|
||||
- [Angular](#version-7x-angular)
|
||||
- [React](#version-7x-react)
|
||||
- [Vue](#version-7x-vue)
|
||||
- [CSS Utilities](#version-7x-css-utilities)
|
||||
- [hidden attribute](#version-7x-hidden-attribute)
|
||||
|
||||
<h2 id="version-7x-browser-platform-support">Browser and Platform Support</h2>
|
||||
|
||||
This section details the desktop browser, JavaScript framework, and mobile platform versions that are supported by Ionic 7.
|
||||
|
||||
**Minimum Browser Versions**
|
||||
| Desktop Browser | Supported Versions |
|
||||
| --------------- | ----------------- |
|
||||
| Chrome | 79+ |
|
||||
| Safari | 14+ |
|
||||
| Firefox | 70+ |
|
||||
| Edge | 79+ |
|
||||
|
||||
**Minimum JavaScript Framework Versions**
|
||||
|
||||
| Framework | Supported Version |
|
||||
| --------- | --------------------- |
|
||||
| Angular | 14+ |
|
||||
| React | 17+ |
|
||||
| Vue | 3.0.6+ |
|
||||
|
||||
**Minimum Mobile Platform Versions**
|
||||
|
||||
| Platform | Supported Version |
|
||||
| -------- | ---------------------- |
|
||||
| iOS | 14+ |
|
||||
| Android | 5.1+ with Chromium 79+ |
|
||||
|
||||
<h2 id="version-7x-components">Components</h2>
|
||||
|
||||
<h4 id="version-7x-accordion-group">Accordion Group</h4>
|
||||
|
||||
- `ionChange` is no longer emitted when the `value` of `ion-accordion-group` is modified externally. `ionChange` is only emitted from user committed changes, such as clicking or tapping the accordion header.
|
||||
|
||||
- Accordion Group no longer automatically adjusts the `value` property when passed an array and `multiple="false"`. Developers should update their apps to ensure they are using the API correctly.
|
||||
|
||||
<h4 id="version-7x-action-sheet">Action Sheet</h4>
|
||||
|
||||
- Action Sheet is updated to align with the design specification.
|
||||
|
||||
**Design tokens**
|
||||
|
||||
| Token | Previous Value | New Value |
|
||||
| ---------- | -------------- | --------- |
|
||||
| `--height` | `100%` | `auto` |
|
||||
|
||||
<h4 id="version-7x-button">Button</h4>
|
||||
|
||||
- Button is updated to align with the design specification for iOS.
|
||||
|
||||
**Design tokens**
|
||||
|
||||
| Token | Previous Value | New Value |
|
||||
| ---------------------------------- | -------------- | --------- |
|
||||
| `$button-ios-letter-spacing` | `-0.03em` | `0` |
|
||||
| `$button-ios-clear-letter-spacing` | `0` | Removed |
|
||||
| `$button-ios-height` | `2.8em` | `3.1em` |
|
||||
| `$button-ios-border-radius` | `10px` | `14px` |
|
||||
| `$button-ios-large-height` | `2.8em` | `3.1em` |
|
||||
| `$button-ios-large-border-radius` | `12px` | `16px` |
|
||||
|
||||
<h4 id="version-7x-back-button">Back Button</h4>
|
||||
|
||||
- Back Button is updated to align with the design specification for iOS.
|
||||
|
||||
**Design tokens**
|
||||
|
||||
| Token | Previous Value | New Value |
|
||||
| ------------------- | -------------- | --------- |
|
||||
| `--icon-margin-end` | `-5px` | `1px` |
|
||||
| `--icon-font-size` | `1.85em` | `1.6em` |
|
||||
|
||||
<h4 id="version-7x-card-header">Card Header</h4>
|
||||
|
||||
- The card header has ben changed to a flex container with direction set to `column` (top to bottom). In `ios` mode the direction is set to `column-reverse` which results in the subtitle displaying on top of the title.
|
||||
|
||||
<h4 id="version-7x-checkbox">Checkbox</h4>
|
||||
|
||||
- `ionChange` is no longer emitted when the `checked` property of `ion-checkbox` is modified externally. `ionChange` is only emitted from user committed changes, such as clicking or tapping the checkbox.
|
||||
|
||||
- The `--background` and `--background-checked` CSS variables have been renamed to `--checkbox-background` and `--checkbox-background-checked` respectively.
|
||||
|
||||
<h4 id="version-7x-datetime">Datetime</h4>
|
||||
|
||||
- `ionChange` is no longer emitted when the `value` property of `ion-datetime` is modified externally. `ionChange` is only emitted from user committed changes, such as clicking or tapping a date.
|
||||
|
||||
- Datetime no longer automatically adjusts the `value` property when passed an array and `multiple="false"`. Developers should update their apps to ensure they are using the API correctly.
|
||||
|
||||
- Datetime no longer incorrectly reports the time zone when `value` is updated. Datetime does not manage time zones, so any time zone information provided is ignored.
|
||||
|
||||
- Passing the empty string to the `value` property will now error as it is not a valid ISO-8601 value.
|
||||
|
||||
- The haptics when swiping the wheel picker are now enabled only on iOS.
|
||||
|
||||
<h4 id="version-7x-input">Input</h4>
|
||||
|
||||
- `ionChange` is no longer emitted when the `value` of `ion-input` is modified externally. `ionChange` is only emitted from user committed changes, such as typing in the input and the input losing focus, clicking the clear action within the input, or pressing the "Enter" key.
|
||||
|
||||
- If your application requires immediate feedback based on the user typing actively in the input, consider migrating your event listeners to using `ionInput` instead.
|
||||
|
||||
- The `debounce` property has been updated to control the timing in milliseconds to delay the event emission of the `ionInput` event after each keystroke. Previously it would delay the event emission of `ionChange`.
|
||||
|
||||
- The `debounce` property's default value has changed from `0` to `undefined`. If `debounce` is undefined, the `ionInput` event will fire immediately.
|
||||
|
||||
- The `detail` payload for the `ionInput` event now contains an object with the current `value` as well as the native event that triggered `ionInput`.
|
||||
|
||||
**Design tokens**
|
||||
|
||||
| Token | Previous Value | New Value |
|
||||
| ----------------------- | -------------- | --------- |
|
||||
| `--placeholder-opacity` | `0.5` | `0.6` |
|
||||
|
||||
<h4 id="version-7x-item">Item</h4>
|
||||
|
||||
**Design tokens**
|
||||
|
||||
iOS:
|
||||
|
||||
| Token | Previous Value | New Value |
|
||||
| --------------------- | -------------- | --------- |
|
||||
| `$item-ios-font-size` | `17px` | `16px` |
|
||||
| `--inner-padding-end` | `10px` | `16px` |
|
||||
| `--padding-start` | `20px` | `16px` |
|
||||
|
||||
<h4 id="version-7x-modal">Modal</h4>
|
||||
|
||||
- The `swipeToClose` property has been removed in favor of `canDismiss`.
|
||||
- The `canDismiss` property now defaults to `true` and can no longer be set to `undefined`.
|
||||
|
||||
<h4 id="version-7x-overlays">Overlays</h4>
|
||||
|
||||
Ionic now listens on the `keydown` event instead of the `keyup` event when determining when to dismiss overlays via the "Escape" key. Any applications that were listening on `keyup` to suppress this behavior should listen on `keydown` instead.
|
||||
|
||||
<h4 id="version-7x-picker">Picker</h4>
|
||||
|
||||
- The `refresh` key has been removed from the `PickerColumn` interface. Developers should use the `columns` property to refresh the `ion-picker` view.
|
||||
|
||||
<h4 id="version-7x-radio-group">Radio Group</h4>
|
||||
|
||||
- `ionChange` is no longer emitted when the `value` of `ion-radio-group` is modified externally. `ionChange` is only emitted from user committed changes, such as clicking or tapping an `ion-radio` in the group.
|
||||
|
||||
<h4 id="version-7x-range">Range</h4>
|
||||
|
||||
- Range is updated to align with the design specification for supported modes.
|
||||
|
||||
**Design tokens**
|
||||
|
||||
iOS:
|
||||
|
||||
| Token | Previous Value | New Value |
|
||||
| --------------------------------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| `--bar-border-radius` | `0px` | `$range-ios-bar-border-radius` (`2px` default) |
|
||||
| `--knob-size` | `28px` | `$range-ios-knob-width` (`26px` default) |
|
||||
| `$range-ios-bar-height` | `2px` | `4px` |
|
||||
| `$range-ios-bar-background-color` | `rgba(var(--ion-text-color-rgb, 0, 0, 0), .1)` | `var(--ion-color-step-900, #e6e6e6)` |
|
||||
| `$range-ios-knob-box-shadow` | `0 3px 1px rgba(0, 0, 0, .1), 0 4px 8px rgba(0, 0, 0, .13), 0 0 0 1px rgba(0, 0, 0, .02)` | `0px 0.5px 4px rgba(0, 0, 0, 0.12), 0px 6px 13px rgba(0, 0, 0, 0.12)` |
|
||||
| `$range-ios-knob-width` | `28px` | `26px` |
|
||||
|
||||
- `ionChange` is no longer emitted when the `value` of `ion-range` is modified externally. `ionChange` is only emitted from user committed changes, such as dragging and releasing the range knob or selecting a new value with the keyboard arrows.
|
||||
- If your application requires immediate feedback based on the user actively dragging the range knob, consider migrating your event listeners to using `ionInput` instead.
|
||||
|
||||
- The `debounce` property's value value has changed from `0` to `undefined`. If `debounce` is undefined, the `ionInput` event will fire immediately.
|
||||
|
||||
- Range no longer clamps assigned values within bounds. Developers will need to validate the value they are assigning to `ion-range` is within the `min` and `max` bounds when programmatically assigning a value.
|
||||
|
||||
- The `name` property defaults to `ion-r-${rangeIds++}` where `rangeIds` is a number that is incremented for every instance of `ion-range`.
|
||||
|
||||
<h4 id="version-7x-searchbar">Searchbar</h4>
|
||||
|
||||
- `ionChange` is no longer emitted when the `value` of `ion-searchbar` is modified externally. `ionChange` is only emitted from user committed changes, such as typing in the searchbar and the searchbar losing focus or pressing the "Enter" key.
|
||||
|
||||
- If your application requires immediate feedback based on the user typing actively in the searchbar, consider migrating your event listeners to using `ionInput` instead.
|
||||
|
||||
- The `debounce` property has been updated to control the timing in milliseconds to delay the event emission of the `ionInput` event after each keystroke. Previously it would delay the event emission of `ionChange`.
|
||||
|
||||
- The `debounce` property's default value has changed from 250 to `undefined`. If `debounce` is undefined, the `ionInput` event will fire immediately.
|
||||
|
||||
- The `detail` payload for the `ionInput` event now contains an object with the current `value` as well as the native event that triggered `ionInput`.
|
||||
|
||||
**Design tokens**
|
||||
|
||||
| Token | Previous Value | New Value |
|
||||
| ----------------------- | -------------- | --------- |
|
||||
| `--placeholder-opacity` | `0.5` | `0.6` |
|
||||
|
||||
|
||||
<h4 id="version-7x-segment">Segment</h4>
|
||||
|
||||
- `ionChange` is no longer emitted when the `value` of `ion-segment` is modified externally. `ionChange` is only emitted from user committed changes, such as clicking a segment button or dragging to activate a segment button.
|
||||
|
||||
- The type signature of `value` supports `string | undefined`. Previously the type signature was `string | null | undefined`.
|
||||
- Developers needing to clear the checked segment item should assign a value of `''` instead of `null`.
|
||||
|
||||
<h4 id="version-7x-select">Select</h4>
|
||||
|
||||
- `ionChange` is no longer emitted when the `value` of `ion-select` is modified externally. `ionChange` is only emitted from user committed changes, such as confirming a selected option in the select's overlay.
|
||||
|
||||
- The `icon` CSS Shadow Part now targets an `ion-icon` component.
|
||||
|
||||
**Design tokens**
|
||||
|
||||
| Token | Previous Value | New Value |
|
||||
| ----------------------- | -------------- | --------- |
|
||||
| `--placeholder-opacity` | `0.33` | `0.6` |
|
||||
|
||||
<h4 id="version-7x-slides">Slides</h4>
|
||||
|
||||
`ion-slides`, `ion-slide`, and the `IonicSwiper` plugin have been removed from Ionic.
|
||||
|
||||
Developers using these components will need to migrate to using Swiper.js directly, optionally using the `IonicSlides` plugin. Guides for migration and usage are linked below:
|
||||
|
||||
- [Angular](https://ionicframework.com/docs/angular/slides)
|
||||
- [React](https://ionicframework.com/docs/react/slides)
|
||||
- [Vue](https://ionicframework.com/docs/vue/slides)
|
||||
|
||||
<h4 id="version-7x-textarea">Textarea</h4>
|
||||
|
||||
- `ionChange` is no longer emitted when the `value` of `ion-textarea` is modified externally. `ionChange` is only emitted from user committed changes, such as typing in the textarea and the textarea losing focus.
|
||||
|
||||
- If your application requires immediate feedback based on the user typing actively in the textarea, consider migrating your event listeners to using `ionInput` instead.
|
||||
|
||||
- The `debounce` property has been updated to control the timing in milliseconds to delay the event emission of the `ionInput` event after each keystroke. Previously it would delay the event emission of `ionChange`.
|
||||
|
||||
- The `debounce` property's default value has changed from `0` to `undefined`. If `debounce` is undefined, the `ionInput` event will fire immediately.
|
||||
|
||||
- `ionInput` dispatches an event detail of `null` when the textarea is cleared as a result of `clear-on-edit="true"`.
|
||||
|
||||
- The `detail` payload for the `ionInput` event now contains an object with the current `value` as well as the native event that triggered `ionInput`.
|
||||
|
||||
**Design tokens**
|
||||
|
||||
| Token | Previous Value | New Value |
|
||||
| ----------------------- | -------------- | --------- |
|
||||
| `--placeholder-opacity` | `0.5` | `0.6` |
|
||||
|
||||
|
||||
<h4 id="version-7x-toggle">Toggle</h4>
|
||||
|
||||
- `ionChange` is no longer emitted when the `checked` property of `ion-toggle` is modified externally. `ionChange` is only emitted from user committed changes, such as clicking the toggle to set it on or off.
|
||||
|
||||
- The `--background` and `--background-checked` variables have been renamed to `--track-background` and `--track-background-checked`, respectively.
|
||||
|
||||
<h4 id="version-7x-virtual-scroll">Virtual Scroll</h4>
|
||||
|
||||
`ion-virtual-scroll` has been removed from Ionic.
|
||||
|
||||
Developers using the component will need to migrate to a virtual scroll solution provided by their framework:
|
||||
|
||||
- [Angular](https://ionicframework.com/docs/angular/virtual-scroll)
|
||||
- [React](https://ionicframework.com/docs/react/virtual-scroll)
|
||||
- [Vue](https://ionicframework.com/docs/vue/virtual-scroll)
|
||||
|
||||
Any references to the virtual scroll types from `@ionic/core` have been removed. Please remove or replace these types: `Cell`, `VirtualNode`, `CellType`, `NodeChange`, `HeaderFn`, `ItemHeightFn`, `FooterHeightFn`, `ItemRenderFn` and `DomRenderFn`.
|
||||
|
||||
<h2 id="version-7x-config">Config</h2>
|
||||
|
||||
- `innerHTMLTemplatesEnabled` defaults to `false`. Developers who wish to use the `innerHTML` functionality inside of `ion-alert`, `ion-infinite-scroll-content`, `ion-loading`, `ion-refresher-content`, and `ion-toast` must set this config to `true` and properly sanitize their content.
|
||||
|
||||
<h2 id="version-7x-types">Types</h2>
|
||||
|
||||
<h4 id="version-7x-overlay-attribute-interfaces">Overlay Attribute Interfaces</h4>
|
||||
|
||||
`ActionSheetAttributes`, `AlertAttributes`, `AlertTextareaAttributes`, `AlertInputAttributes`, `LoadingAttributes`, `ModalAttributes`, `PickerAttributes`, `PopoverAttributes`, and `ToastAttributes` have been removed. Developers should use `{ [key: string]: any }` instead.
|
||||
|
||||
<h2 id="version-7x-javascript-frameworks">JavaScript Frameworks</h2>
|
||||
|
||||
<h4 id="version-7x-angular">Angular</h4>
|
||||
|
||||
- Angular v14 is now required to use `@ionic/angular` and `@ionic/angular-server`. Upgrade your project to Angular v14 by following the [Angular v14 update guide](https://update.angular.io/?l=3&v=13.0-14.0).
|
||||
|
||||
- `null` values on form components will no longer be converted to the empty string (`''`) or `false`. This impacts `ion-checkbox`, `ion-datetime`, `ion-input`, `ion-radio`, `ion-radio-group`, `ion-range`, `ion-searchbar`, `ion-segment`, `ion-select`, `ion-textarea`, and `ion-toggle`.
|
||||
|
||||
- The dev-preview `environmentInjector` property has been removed from `ion-tabs` and `ion-router-outlet`. Standalone component routing is now available without additional custom configuration. Remove the `environmentInjector` property from your `ion-tabs` and `ion-router-outlet` components.
|
||||
|
||||
<h4 id="version-7x-react">React</h4>
|
||||
|
||||
`@ionic/react` and `@ionic/react-router` no longer ship a CommonJS entry point. Instead, only an ES Module entry point is provided for improved compatibility with Vite.
|
||||
|
||||
<h4 id="version-7x-vue">Vue</h4>
|
||||
|
||||
`@ionic/vue` and `@ionic/vue-router` no longer ship a CommonJS entry point. Instead, only an ES Module entry point is provided for improved compatibility with Vite.
|
||||
|
||||
<h2 id="version-7x-css-utilities">CSS Utilities</h2>
|
||||
|
||||
<h4 id="version-7x-hidden-attribute">`hidden` attribute</h4>
|
||||
|
||||
The `[hidden]` attribute has been removed from Ionic's global stylesheet. The `[hidden]` attribute can continue to be used, but developers will get the [native `hidden` implementation](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/hidden) instead. The main difference is that the native implementation is easier to override using `display` than Ionic's implementation.
|
||||
|
||||
Developers can add the following CSS to their global stylesheet if they need the old behavior:
|
||||
|
||||
```css
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
```
|
||||
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',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { Component, Event, Host, Listen, Prop, h } from '@stencil/core';
|
||||
import { GESTURE_CONTROLLER } from '@utils/gesture';
|
||||
|
||||
import { getIonMode } from '../../global/ionic-global';
|
||||
|
||||
@@ -13,10 +12,6 @@ import { getIonMode } from '../../global/ionic-global';
|
||||
shadow: true,
|
||||
})
|
||||
export class Backdrop implements ComponentInterface {
|
||||
private blocker = GESTURE_CONTROLLER.createBlocker({
|
||||
disableScroll: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* If `true`, the backdrop will be visible.
|
||||
*/
|
||||
@@ -37,16 +32,6 @@ export class Backdrop implements ComponentInterface {
|
||||
*/
|
||||
@Event() ionBackdropTap!: EventEmitter<void>;
|
||||
|
||||
connectedCallback() {
|
||||
if (this.stopPropagation) {
|
||||
this.blocker.block();
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.blocker.unblock();
|
||||
}
|
||||
|
||||
@Listen('click', { passive: false, capture: true })
|
||||
protected onMouseDown(ev: TouchEvent) {
|
||||
this.emitTap(ev);
|
||||
|
||||
@@ -54,10 +54,6 @@
|
||||
color: current-color(contrast);
|
||||
}
|
||||
|
||||
:host(.outer-content) {
|
||||
--background: #{$background-color-step-50};
|
||||
}
|
||||
|
||||
#background-content {
|
||||
@include position(calc(var(--offset-top) * -1), 0px,calc(var(--offset-bottom) * -1), 0px);
|
||||
|
||||
|
||||
@@ -34,16 +34,24 @@
|
||||
|
||||
// Calendar / Header / Action Buttons
|
||||
// -----------------------------------
|
||||
:host .calendar-action-buttons ion-item {
|
||||
--padding-start: #{$datetime-ios-padding};
|
||||
--background-hover: transparent;
|
||||
--background-activated: transparent;
|
||||
.calendar-month-year-toggle {
|
||||
@include padding(0px, 16px, 0px, #{$datetime-ios-padding});
|
||||
|
||||
min-height: 44px;
|
||||
|
||||
font-size: dynamic-font-max(16px, 1.6);
|
||||
font-weight: 600;
|
||||
|
||||
&.ion-focused::after {
|
||||
opacity: 0.15;
|
||||
}
|
||||
}
|
||||
|
||||
:host .calendar-action-buttons ion-item ion-icon,
|
||||
.calendar-month-year-toggle #toggle-wrapper {
|
||||
@include margin(10px, 8px, 10px, 0);
|
||||
}
|
||||
|
||||
:host .calendar-action-buttons .calendar-month-year-toggle ion-icon,
|
||||
:host .calendar-action-buttons ion-buttons ion-button {
|
||||
color: current-color(base);
|
||||
}
|
||||
|
||||
@@ -30,15 +30,41 @@
|
||||
|
||||
// Calendar / Header / Action Buttons
|
||||
// -----------------------------------
|
||||
:host .datetime-calendar .calendar-action-buttons ion-item {
|
||||
--padding-start: #{$datetime-md-header-padding};
|
||||
}
|
||||
|
||||
:host .calendar-action-buttons ion-item,
|
||||
:host .calendar-action-buttons ion-button {
|
||||
--color: #{$text-color-step-350};
|
||||
}
|
||||
|
||||
.calendar-month-year-toggle {
|
||||
@include padding(12px, 16px, 12px, #{$datetime-md-header-padding});
|
||||
|
||||
min-height: 48px;
|
||||
|
||||
background: transparent;
|
||||
|
||||
color: #{$text-color-step-350};
|
||||
|
||||
z-index: 1;
|
||||
|
||||
&.ion-focused::after {
|
||||
opacity: 0.04;
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-month-year-toggle ion-ripple-effect {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
@media (any-hover: hover) {
|
||||
.calendar-month-year-toggle.ion-activatable:not(.ion-focused):hover {
|
||||
&::after {
|
||||
background: currentColor;
|
||||
|
||||
opacity: 0.04;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calendar / Header / Days of Week
|
||||
// -----------------------------------
|
||||
:host .calendar-days-of-week {
|
||||
@@ -64,7 +90,6 @@
|
||||
* if necessary.
|
||||
*/
|
||||
grid-template-rows: repeat(6, 1fr);
|
||||
|
||||
}
|
||||
|
||||
// Individual day button in month
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ ion-picker-column-internal {
|
||||
}
|
||||
|
||||
/**
|
||||
* This ensures that the picker is apppropriately
|
||||
* This ensures that the picker is appropriately
|
||||
* sized and never truncates the text.
|
||||
*/
|
||||
:host(.datetime-size-fixed.datetime-prefer-wheel) {
|
||||
@@ -267,19 +267,8 @@ ion-picker-column-internal {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
:host .calendar-action-buttons ion-item,
|
||||
:host .calendar-action-buttons ion-button {
|
||||
--background: translucent;
|
||||
}
|
||||
|
||||
:host .calendar-action-buttons ion-item ion-label {
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:host .calendar-action-buttons ion-item ion-icon {
|
||||
@include padding(0, 0, 0, 4px);
|
||||
--background: transparent;
|
||||
}
|
||||
|
||||
// Calendar / Header / Days of Week
|
||||
@@ -488,6 +477,55 @@ ion-picker-column-internal {
|
||||
|
||||
// Year Picker
|
||||
// -----------------------------------
|
||||
:host(.show-month-and-year) .calendar-action-buttons ion-item {
|
||||
--color: #{current-color(base)};
|
||||
:host(.show-month-and-year) .calendar-action-buttons .calendar-month-year-toggle {
|
||||
color: #{current-color(base)};
|
||||
}
|
||||
|
||||
.calendar-month-year {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.calendar-month-year-toggle {
|
||||
@include text-inherit();
|
||||
|
||||
position: relative;
|
||||
|
||||
border: 0;
|
||||
|
||||
outline: none;
|
||||
|
||||
background: transparent;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
z-index: 1;
|
||||
|
||||
&::after {
|
||||
@include button-state();
|
||||
|
||||
transition: opacity 15ms linear, background-color 15ms linear;
|
||||
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&.ion-focused::after {
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-month-year-toggle ion-icon {
|
||||
@include padding(0, 0, 0, 4px);
|
||||
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.calendar-month-year-toggle #toggle-wrapper {
|
||||
display: inline-flex;
|
||||
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -104,7 +104,6 @@ import {
|
||||
export class Datetime implements ComponentInterface {
|
||||
private inputId = `ion-dt-${datetimeIds++}`;
|
||||
private calendarBodyRef?: HTMLElement;
|
||||
private monthYearToggleItemRef?: HTMLIonItemElement;
|
||||
private popoverRef?: HTMLIonPopoverElement;
|
||||
private clearFocusVisible?: () => void;
|
||||
private parsedMinuteValues?: number[];
|
||||
@@ -1528,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) {
|
||||
@@ -1614,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}
|
||||
@@ -1648,7 +1647,7 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
></ion-picker-column-internal>
|
||||
></ion-picker-column>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1734,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}
|
||||
@@ -1765,7 +1764,7 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
></ion-picker-column-internal>
|
||||
></ion-picker-column>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1779,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}
|
||||
@@ -1810,7 +1809,7 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
></ion-picker-column-internal>
|
||||
></ion-picker-column>
|
||||
);
|
||||
}
|
||||
private renderYearPickerColumn(years: PickerColumnItem[]) {
|
||||
@@ -1823,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}
|
||||
@@ -1854,7 +1853,7 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
></ion-picker-column-internal>
|
||||
></ion-picker-column>
|
||||
);
|
||||
}
|
||||
private renderTimePickerColumns(forcePresentation: string) {
|
||||
@@ -1898,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}
|
||||
@@ -1917,7 +1916,7 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
></ion-picker-column-internal>
|
||||
></ion-picker-column>
|
||||
);
|
||||
}
|
||||
private renderMinutePickerColumn(minutesData: PickerColumnItem[]) {
|
||||
@@ -1927,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}
|
||||
@@ -1946,7 +1945,7 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
></ion-picker-column-internal>
|
||||
></ion-picker-column>
|
||||
);
|
||||
}
|
||||
private renderDayPeriodPickerColumn(dayPeriodData: PickerColumnItem[]) {
|
||||
@@ -1959,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}
|
||||
@@ -1982,7 +1981,7 @@ export class Datetime implements ComponentInterface {
|
||||
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
></ion-picker-column-internal>
|
||||
></ion-picker-column>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2020,35 +2019,18 @@ export class Datetime implements ComponentInterface {
|
||||
<div class="calendar-header">
|
||||
<div class="calendar-action-buttons">
|
||||
<div class="calendar-month-year">
|
||||
<ion-item
|
||||
part="month-year-button"
|
||||
ref={(el) => (this.monthYearToggleItemRef = el)}
|
||||
button
|
||||
aria-label="Show year picker"
|
||||
detail={false}
|
||||
lines="none"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
this.toggleMonthAndYearView();
|
||||
/**
|
||||
* TODO: FW-3547
|
||||
*
|
||||
* Currently there is not a way to set the aria-label on the inner button
|
||||
* on the `ion-item` and have it be reactive to changes. This is a workaround
|
||||
* until we either refactor `ion-item` to a button or Stencil adds a way to
|
||||
* have reactive props for built-in properties, such as `aria-label`.
|
||||
*/
|
||||
const { monthYearToggleItemRef } = this;
|
||||
if (monthYearToggleItemRef) {
|
||||
const btn = monthYearToggleItemRef.shadowRoot?.querySelector('.item-native');
|
||||
if (btn) {
|
||||
const monthYearAriaLabel = this.showMonthAndYear ? 'Hide year picker' : 'Show year picker';
|
||||
btn.setAttribute('aria-label', monthYearAriaLabel);
|
||||
}
|
||||
}
|
||||
<button
|
||||
class={{
|
||||
'calendar-month-year-toggle': true,
|
||||
'ion-activatable': true,
|
||||
'ion-focusable': true,
|
||||
}}
|
||||
part="month-year-button"
|
||||
disabled={disabled}
|
||||
aria-label={this.showMonthAndYear ? 'Hide year picker' : 'Show year picker'}
|
||||
onClick={() => this.toggleMonthAndYearView()}
|
||||
>
|
||||
<ion-label>
|
||||
<span id="toggle-wrapper">
|
||||
{getMonthAndYear(this.locale, this.workingParts)}
|
||||
<ion-icon
|
||||
aria-hidden="true"
|
||||
@@ -2056,8 +2038,9 @@ export class Datetime implements ComponentInterface {
|
||||
lazy={false}
|
||||
flipRtl={true}
|
||||
></ion-icon>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</span>
|
||||
{mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="calendar-next-prev">
|
||||
@@ -2362,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());
|
||||
}}
|
||||
|
||||
@@ -47,7 +47,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
|
||||
|
||||
const datetime = page.locator('ion-datetime');
|
||||
const monthYearButton = page.locator('.calendar-month-year ion-item');
|
||||
const monthYearButton = page.locator('.calendar-month-year-toggle');
|
||||
const prevButton = page.locator('.calendar-next-prev ion-button:nth-child(1)');
|
||||
const nextButton = page.locator('.calendar-next-prev ion-button:nth-child(2)');
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
@@ -1,7 +1,6 @@
|
||||
import type { SpecPage } from '@stencil/core/testing';
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
|
||||
import { Item } from '../../../item/item';
|
||||
import { Datetime } from '../../datetime';
|
||||
|
||||
describe('datetime', () => {
|
||||
@@ -20,15 +19,14 @@ describe('datetime', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
page = await newSpecPage({
|
||||
components: [Datetime, Item],
|
||||
components: [Datetime],
|
||||
html: `<ion-datetime></ion-datetime>`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should have aria-label "Show year picker" when collapsed', async () => {
|
||||
const datetime = page.body.querySelector('ion-datetime')!;
|
||||
const item = datetime.shadowRoot!.querySelector('.calendar-month-year ion-item');
|
||||
const monthYearToggleBtn = item!.shadowRoot!.querySelector('button');
|
||||
const monthYearToggleBtn = datetime.shadowRoot!.querySelector('.calendar-month-year .calendar-month-year-toggle');
|
||||
const ariaLabel = monthYearToggleBtn!.getAttribute('aria-label');
|
||||
|
||||
expect(ariaLabel).toContain('Show year picker');
|
||||
@@ -36,15 +34,18 @@ describe('datetime', () => {
|
||||
|
||||
it('should have aria-label "Hide year picker" when expanded', async () => {
|
||||
const datetime = page.body.querySelector('ion-datetime')!;
|
||||
const item = datetime.shadowRoot!.querySelector<HTMLIonItemElement>('.calendar-month-year ion-item');
|
||||
const monthYearToggleBtn = datetime.shadowRoot!.querySelector<HTMLButtonElement>(
|
||||
'.calendar-month-year .calendar-month-year-toggle'
|
||||
);
|
||||
|
||||
item!.click();
|
||||
monthYearToggleBtn!.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
const itemAfter = datetime.shadowRoot!.querySelector<HTMLIonItemElement>('.calendar-month-year ion-item');
|
||||
const monthYearToggleBtn = itemAfter!.shadowRoot!.querySelector<HTMLElement>('button');
|
||||
const ariaLabel = monthYearToggleBtn!.getAttribute('aria-label');
|
||||
const monthYearToggleBtnAfter = datetime.shadowRoot!.querySelector<HTMLButtonElement>(
|
||||
'.calendar-month-year .calendar-month-year-toggle'
|
||||
);
|
||||
const ariaLabel = monthYearToggleBtnAfter!.getAttribute('aria-label');
|
||||
|
||||
expect(ariaLabel).toContain('Hide year picker');
|
||||
});
|
||||
|
||||
@@ -526,4 +526,20 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
await expect(datetime).toHaveScreenshot(screenshot(`datetime-focus-calendar-day`));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe(title('datetime: calendar month toggle'), () => {
|
||||
test('should have focus styles', async ({ page }) => {
|
||||
await page.setContent('<ion-datetime value="2021-01-01"></ion-datetime>', config);
|
||||
|
||||
const datetime = page.locator('ion-datetime');
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
|
||||
const monthYearToggle = datetime.locator('.calendar-month-year-toggle');
|
||||
|
||||
monthYearToggle.evaluate((el: HTMLElement) => el.classList.add('ion-focused'));
|
||||
|
||||
await expect(datetime).toHaveScreenshot(screenshot(`date-month-toggle-focused`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -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);
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
const datetimeFooter = page.locator('#date-time .datetime-footer');
|
||||
await expect(datetimeFooter).toBeVisible();
|
||||
|
||||
const pickerButton = page.locator('#date-time .calendar-month-year > ion-item');
|
||||
const pickerButton = page.locator('#date-time .calendar-month-year > .calendar-month-year-toggle');
|
||||
await pickerButton.click();
|
||||
await page.waitForChanges();
|
||||
await expect(datetimeFooter).not.toBeVisible();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -68,7 +68,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config, scree
|
||||
|
||||
await page.waitForSelector('.datetime-ready');
|
||||
const calendarMonthYear = page.locator('ion-datetime .calendar-month-year');
|
||||
const monthYearButton = page.locator('.calendar-month-year ion-item');
|
||||
const monthYearButton = page.locator('.calendar-month-year-toggle');
|
||||
await expect(calendarMonthYear).toHaveText('February 2022');
|
||||
|
||||
await page.keyboard.press(tabKey);
|
||||
@@ -114,7 +114,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config, scree
|
||||
const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab';
|
||||
|
||||
const datetime = page.locator('ion-datetime');
|
||||
const monthYearButton = page.locator('.calendar-month-year ion-item');
|
||||
const monthYearButton = page.locator('.calendar-month-year-toggle');
|
||||
const prevButton = page.locator('.calendar-next-prev ion-button:nth-child(1)');
|
||||
const nextButton = page.locator('.calendar-next-prev ion-button:nth-child(2)');
|
||||
const calendarMonthYear = page.locator('ion-datetime .calendar-month-year');
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -52,7 +52,7 @@ export class Menu implements ComponentInterface, MenuI {
|
||||
width!: number;
|
||||
_isOpen = false;
|
||||
|
||||
backdropEl?: HTMLElement;
|
||||
backdropEl?: HTMLIonBackdropElement;
|
||||
menuInnerEl?: HTMLElement;
|
||||
contentEl?: HTMLElement;
|
||||
lastFocus?: HTMLElement;
|
||||
|
||||
@@ -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>
|
||||