Compare commits

..

1 Commits

Author SHA1 Message Date
Sean Perkins
395b14317f chore: use dev-build 2023-11-15 12:56:32 -05:00
335 changed files with 5534 additions and 10692 deletions

4
.github/CODEOWNERS vendored
View File

@@ -51,8 +51,8 @@
/core/src/components/nav/ @sean-perkins
/core/src/components/nav-link/ @sean-perkins
/core/src/components/picker/ @liamdebeasi
/core/src/components/picker-column/ @liamdebeasi
/core/src/components/picker-internal/ @liamdebeasi
/core/src/components/picker-column-internal/ @liamdebeasi
/core/src/components/radio/ @amandaejohnston
/core/src/components/radio-group/ @amandaejohnston

View File

@@ -4,65 +4,338 @@ This is a comprehensive list of the breaking changes introduced in the major ver
## Versions
- [Version 8.x](#version-8x)
- [Version 7.x](./BREAKING_ARCHIVE/v7.md)
- [Version 7.x](#version-7x)
- [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 8.x
## Version 7.x
- [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)
- [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-8x-browser-platform-support">Browser and Platform Support</h2>
<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 8.
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 | 89+ |
| Safari | 15+ |
| Firefox | 75+ |
| Edge | 89+ |
| Chrome | 79+ |
| Safari | 14+ |
| Firefox | 70+ |
| Edge | 79+ |
**Minimum JavaScript Framework Versions**
| Framework | Supported Version |
| --------- | --------------------- |
| Angular | 16+ |
| Angular | 14+ |
| React | 17+ |
| Vue | 3.0.6+ |
**Minimum Mobile Platform Versions**
| Platform | Supported Version |
| -------- | ---------------------- |
| iOS | 15+ |
| Android | 5.1+ with Chromium 89+ |
| iOS | 14+ |
| Android | 5.1+ with Chromium 79+ |
<h2 id="version-8x-components">Components</h2>
<h2 id="version-7x-components">Components</h2>
<h4 id="version-8x-content">Content</h4>
<h4 id="version-7x-accordion-group">Accordion Group</h4>
- Content no longer sets the `--background` custom property when the `.outer-content` class is set on the host.
- `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.
<h4 id="version-8x-datetime">Datetime</h4>
- 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.
- 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;
<h4 id="version-7x-action-sheet">Action Sheet</h4>
+ background: red;
}
```
<h2 id="version-8x-picker">Picker</h2>
- Action Sheet is updated to align with the design specification.
- `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.
**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;
}
```

View File

@@ -1,331 +0,0 @@
# 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;
}
```

View File

@@ -3,20 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [7.5.6](https://github.com/ionic-team/ionic-framework/compare/v7.5.5...v7.5.6) (2023-11-21)
### Bug Fixes
* **alert:** match MD spec on tablet ([#28501](https://github.com/ionic-team/ionic-framework/issues/28501)) ([6a2be9f](https://github.com/ionic-team/ionic-framework/commit/6a2be9fa3c12a893d98dc139a1575a6e7e3c7c26)), closes [#23977](https://github.com/ionic-team/ionic-framework/issues/23977)
* **angular:** ng add @ionic/angular in standalone projects ([#28523](https://github.com/ionic-team/ionic-framework/issues/28523)) ([c07312e](https://github.com/ionic-team/ionic-framework/commit/c07312e5ed931f6f825ccf083c9dead9fa815843)), closes [#28514](https://github.com/ionic-team/ionic-framework/issues/28514)
* **angular:** overlays are defined when using standalone controllers ([#28560](https://github.com/ionic-team/ionic-framework/issues/28560)) ([9453132](https://github.com/ionic-team/ionic-framework/commit/9453132aa8952b4adfa1326e61138b329e254f76)), closes [#28385](https://github.com/ionic-team/ionic-framework/issues/28385)
* **datetime:** updating value with min scrolls to new value ([#28549](https://github.com/ionic-team/ionic-framework/issues/28549)) ([388d19e](https://github.com/ionic-team/ionic-framework/commit/388d19e04f83f85abd4602adb04cc71ac575764a)), closes [#28548](https://github.com/ionic-team/ionic-framework/issues/28548)
## [7.5.5](https://github.com/ionic-team/ionic-framework/compare/v7.5.4...v7.5.5) (2023-11-15)

View File

@@ -3,18 +3,6 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [7.5.6](https://github.com/ionic-team/ionic-framework/compare/v7.5.5...v7.5.6) (2023-11-21)
### Bug Fixes
* **alert:** match MD spec on tablet ([#28501](https://github.com/ionic-team/ionic-framework/issues/28501)) ([6a2be9f](https://github.com/ionic-team/ionic-framework/commit/6a2be9fa3c12a893d98dc139a1575a6e7e3c7c26)), closes [#23977](https://github.com/ionic-team/ionic-framework/issues/23977)
* **datetime:** updating value with min scrolls to new value ([#28549](https://github.com/ionic-team/ionic-framework/issues/28549)) ([388d19e](https://github.com/ionic-team/ionic-framework/commit/388d19e04f83f85abd4602adb04cc71ac575764a)), closes [#28548](https://github.com/ionic-team/ionic-framework/issues/28548)
## [7.5.5](https://github.com/ionic-team/ionic-framework/compare/v7.5.4...v7.5.5) (2023-11-15)

View File

@@ -906,64 +906,47 @@ 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,shadow
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,prop,mode,"ios" | "md",undefined,false,false
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-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-popover,shadow
ion-popover,prop,alignment,"center" | "end" | "start" | undefined,undefined,false,false

1625
core/package-lock.json generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/core",
"version": "7.5.6",
"version": "7.5.5",
"description": "Base components for Ionic",
"keywords": [
"ionic",
@@ -31,7 +31,7 @@
"loader/"
],
"dependencies": {
"@stencil/core": "^4.7.2",
"@stencil/core": "^4.6.0-dev.1698410852.c526078",
"ionicons": "^7.2.1",
"tslib": "^2.1.0"
},
@@ -64,7 +64,6 @@
"jest": "^29.7.0",
"jest-cli": "^29.7.0",
"prettier": "^2.6.1",
"puppeteer": "21.1.1",
"rollup": "^2.26.4",
"sass": "^1.33.0",
"serve": "^14.0.1",

View File

@@ -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 { 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 { 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 { 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 { 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 { 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 { 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,58 +1949,6 @@ 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.
*/
@@ -2084,12 +2032,46 @@ export namespace Components {
*/
"trigger": string | undefined;
}
interface IonPickerLegacyColumn {
interface IonPickerColumn {
/**
* 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;
/**
* 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.
@@ -3344,13 +3326,13 @@ export interface IonPickerColumnCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLIonPickerColumnElement;
}
export interface IonPickerLegacyCustomEvent<T> extends CustomEvent<T> {
export interface IonPickerColumnInternalCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLIonPickerLegacyElement;
target: HTMLIonPickerColumnInternalElement;
}
export interface IonPickerLegacyColumnCustomEvent<T> extends CustomEvent<T> {
export interface IonPickerInternalCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLIonPickerLegacyColumnElement;
target: HTMLIonPickerInternalElement;
}
export interface IonPopoverCustomEvent<T> extends CustomEvent<T> {
detail: T;
@@ -4034,7 +4016,14 @@ declare global {
new (): HTMLIonNoteElement;
};
interface HTMLIonPickerElementEventMap {
"ionInputModeChange": PickerChangeEventDetail;
"ionPickerDidPresent": void;
"ionPickerWillPresent": void;
"ionPickerWillDismiss": OverlayEventDetail;
"ionPickerDidDismiss": OverlayEventDetail;
"didPresent": void;
"willPresent": void;
"willDismiss": OverlayEventDetail;
"didDismiss": OverlayEventDetail;
}
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;
@@ -4051,7 +4040,7 @@ declare global {
new (): HTMLIonPickerElement;
};
interface HTMLIonPickerColumnElementEventMap {
"ionChange": string | number | undefined;
"ionPickerColChange": PickerColumn;
}
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;
@@ -4067,52 +4056,39 @@ declare global {
prototype: HTMLIonPickerColumnElement;
new (): HTMLIonPickerColumnElement;
};
interface HTMLIonPickerColumnOptionElement extends Components.IonPickerColumnOption, HTMLStencilElement {
interface HTMLIonPickerColumnInternalElementEventMap {
"ionChange": PickerColumnItem;
}
var HTMLIonPickerColumnOptionElement: {
prototype: HTMLIonPickerColumnOptionElement;
new (): HTMLIonPickerColumnOptionElement;
};
interface HTMLIonPickerLegacyElementEventMap {
"ionPickerDidPresent": void;
"ionPickerWillPresent": void;
"ionPickerWillDismiss": OverlayEventDetail;
"ionPickerDidDismiss": OverlayEventDetail;
"didPresent": void;
"willPresent": void;
"willDismiss": OverlayEventDetail;
"didDismiss": OverlayEventDetail;
}
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;
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 HTMLIonPickerLegacyElementEventMap>(type: K, listener: (this: HTMLIonPickerLegacyElement, ev: IonPickerLegacyCustomEvent<HTMLIonPickerLegacyElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): 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 HTMLIonPickerLegacyElement: {
prototype: HTMLIonPickerLegacyElement;
new (): HTMLIonPickerLegacyElement;
var HTMLIonPickerColumnInternalElement: {
prototype: HTMLIonPickerColumnInternalElement;
new (): HTMLIonPickerColumnInternalElement;
};
interface HTMLIonPickerLegacyColumnElementEventMap {
"ionPickerColChange": PickerColumn;
interface HTMLIonPickerInternalElementEventMap {
"ionInputModeChange": PickerInternalChangeEventDetail;
}
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;
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;
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 HTMLIonPickerInternalElementEventMap>(type: K, listener: (this: HTMLIonPickerInternalElement, ev: IonPickerInternalCustomEvent<HTMLIonPickerInternalElementEventMap[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;
var HTMLIonPickerInternalElement: {
prototype: HTMLIonPickerInternalElement;
new (): HTMLIonPickerInternalElement;
};
interface HTMLIonPopoverElementEventMap {
"ionPopoverDidPresent": void;
@@ -4667,9 +4643,8 @@ declare global {
"ion-note": HTMLIonNoteElement;
"ion-picker": HTMLIonPickerElement;
"ion-picker-column": HTMLIonPickerColumnElement;
"ion-picker-column-option": HTMLIonPickerColumnOptionElement;
"ion-picker-legacy": HTMLIonPickerLegacyElement;
"ion-picker-legacy-column": HTMLIonPickerLegacyColumnElement;
"ion-picker-column-internal": HTMLIonPickerColumnInternalElement;
"ion-picker-internal": HTMLIonPickerInternalElement;
"ion-popover": HTMLIonPopoverElement;
"ion-progress-bar": HTMLIonProgressBarElement;
"ion-radio": HTMLIonRadioElement;
@@ -6601,57 +6576,6 @@ 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.
*/
@@ -6705,35 +6629,35 @@ declare namespace LocalJSX {
/**
* Emitted after the picker has dismissed. Shorthand for ionPickerDidDismiss.
*/
"onDidDismiss"?: (event: IonPickerLegacyCustomEvent<OverlayEventDetail>) => void;
"onDidDismiss"?: (event: IonPickerCustomEvent<OverlayEventDetail>) => void;
/**
* Emitted after the picker has presented. Shorthand for ionPickerWillDismiss.
*/
"onDidPresent"?: (event: IonPickerLegacyCustomEvent<void>) => void;
"onDidPresent"?: (event: IonPickerCustomEvent<void>) => void;
/**
* Emitted after the picker has dismissed.
*/
"onIonPickerDidDismiss"?: (event: IonPickerLegacyCustomEvent<OverlayEventDetail>) => void;
"onIonPickerDidDismiss"?: (event: IonPickerCustomEvent<OverlayEventDetail>) => void;
/**
* Emitted after the picker has presented.
*/
"onIonPickerDidPresent"?: (event: IonPickerLegacyCustomEvent<void>) => void;
"onIonPickerDidPresent"?: (event: IonPickerCustomEvent<void>) => void;
/**
* Emitted before the picker has dismissed.
*/
"onIonPickerWillDismiss"?: (event: IonPickerLegacyCustomEvent<OverlayEventDetail>) => void;
"onIonPickerWillDismiss"?: (event: IonPickerCustomEvent<OverlayEventDetail>) => void;
/**
* Emitted before the picker has presented.
*/
"onIonPickerWillPresent"?: (event: IonPickerLegacyCustomEvent<void>) => void;
"onIonPickerWillPresent"?: (event: IonPickerCustomEvent<void>) => void;
/**
* Emitted before the picker has dismissed. Shorthand for ionPickerWillDismiss.
*/
"onWillDismiss"?: (event: IonPickerLegacyCustomEvent<OverlayEventDetail>) => void;
"onWillDismiss"?: (event: IonPickerCustomEvent<OverlayEventDetail>) => void;
/**
* Emitted before the picker has presented. Shorthand for ionPickerWillPresent.
*/
"onWillPresent"?: (event: IonPickerLegacyCustomEvent<void>) => void;
"onWillPresent"?: (event: IonPickerCustomEvent<void>) => void;
"overlayIndex": number;
/**
* If `true`, a backdrop will be displayed behind the picker.
@@ -6744,7 +6668,7 @@ declare namespace LocalJSX {
*/
"trigger"?: string | undefined;
}
interface IonPickerLegacyColumn {
interface IonPickerColumn {
/**
* Picker column data
*/
@@ -6752,7 +6676,40 @@ declare namespace LocalJSX {
/**
* Emitted when the selected value has changed
*/
"onIonPickerColChange"?: (event: IonPickerLegacyColumnCustomEvent<PickerColumn>) => void;
"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;
/**
* 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;
}
interface IonPopover {
/**
@@ -8121,9 +8078,8 @@ declare namespace LocalJSX {
"ion-note": IonNote;
"ion-picker": IonPicker;
"ion-picker-column": IonPickerColumn;
"ion-picker-column-option": IonPickerColumnOption;
"ion-picker-legacy": IonPickerLegacy;
"ion-picker-legacy-column": IonPickerLegacyColumn;
"ion-picker-column-internal": IonPickerColumnInternal;
"ion-picker-internal": IonPickerInternal;
"ion-popover": IonPopover;
"ion-progress-bar": IonProgressBar;
"ion-radio": IonRadio;
@@ -8219,9 +8175,8 @@ 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-option": LocalJSX.IonPickerColumnOption & JSXBase.HTMLAttributes<HTMLIonPickerColumnOptionElement>;
"ion-picker-legacy": LocalJSX.IonPickerLegacy & JSXBase.HTMLAttributes<HTMLIonPickerLegacyElement>;
"ion-picker-legacy-column": LocalJSX.IonPickerLegacyColumn & JSXBase.HTMLAttributes<HTMLIonPickerLegacyColumnElement>;
"ion-picker-column-internal": LocalJSX.IonPickerColumnInternal & JSXBase.HTMLAttributes<HTMLIonPickerColumnInternalElement>;
"ion-picker-internal": LocalJSX.IonPickerInternal & JSXBase.HTMLAttributes<HTMLIonPickerInternalElement>;
"ion-popover": LocalJSX.IonPopover & JSXBase.HTMLAttributes<HTMLIonPopoverElement>;
"ion-progress-bar": LocalJSX.IonProgressBar & JSXBase.HTMLAttributes<HTMLIonProgressBarElement>;
"ion-radio": LocalJSX.IonRadio & JSXBase.HTMLAttributes<HTMLIonRadioElement>;

View File

@@ -1,8 +1,8 @@
import { newSpecPage } from '@stencil/core/testing';
import { AccordionGroup } from '../../accordion-group/accordion-group';
import { Item } from '../../item/item';
import { Accordion } from '../accordion';
import { AccordionGroup } from '../../accordion-group/accordion-group.tsx';
import { Item } from '../../item/item.tsx';
import { Accordion } from '../accordion.tsx';
it('should open correct accordions when accordion group value is set', async () => {
const page = await newSpecPage({
@@ -25,7 +25,7 @@ it('should open correct accordions when accordion group value is set', async ()
`,
});
const accordionGroup = page.body.querySelector('ion-accordion-group')!;
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordions = accordionGroup.querySelectorAll('ion-accordion');
accordions.forEach((accordion) => {
@@ -61,7 +61,7 @@ it('should open correct accordions when accordion value is set', async () => {
`,
});
const accordionGroup = page.body.querySelector('ion-accordion-group')!;
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordions = accordionGroup.querySelectorAll('ion-accordion');
accordions.forEach((accordion) => {
@@ -97,7 +97,7 @@ it('should open more than one accordion when multiple="true"', async () => {
`,
});
const accordionGroup = page.body.querySelector('ion-accordion-group')!;
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordions = accordionGroup.querySelectorAll('ion-accordion');
accordions.forEach((accordion) => {
@@ -133,7 +133,7 @@ it('should render with accordion open', async () => {
`,
});
const accordionGroup = page.body.querySelector('ion-accordion-group')!;
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordions = accordionGroup.querySelectorAll('ion-accordion');
expect(accordions[0].classList.contains('accordion-collapsed')).toEqual(false);
@@ -162,7 +162,7 @@ it('should accept a string when multiple="true"', async () => {
`,
});
const accordionGroup = page.body.querySelector('ion-accordion-group')!;
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordions = accordionGroup.querySelectorAll('ion-accordion');
expect(accordions[0].classList.contains('accordion-collapsed')).toEqual(false);
@@ -183,8 +183,8 @@ it('should set default values if not provided', async () => {
`,
});
const accordionGroup = page.body.querySelector('ion-accordion-group')!;
const accordion = accordionGroup.querySelector('ion-accordion')!;
const accordionGroup = page.body.querySelector('ion-accordion-group');
const accordion = accordionGroup.querySelector('ion-accordion');
/**
* ID is determined via an auto incrementing counter

View File

@@ -7,17 +7,10 @@ describe('action sheet: htmlAttributes inheritance', () => {
it('should correctly inherit attributes on host', async () => {
const page = await newSpecPage({
components: [ActionSheet],
template: () => (
<ion-action-sheet
htmlAttributes={{
'data-testid': 'basic-action-sheet',
}}
overlayIndex={1}
></ion-action-sheet>
),
template: () => <ion-action-sheet htmlAttributes={{ 'data-testid': 'basic-action-sheet' }}></ion-action-sheet>,
});
const actionSheet = page.body.querySelector('ion-action-sheet')!;
const actionSheet = page.body.querySelector('ion-action-sheet');
await expect(actionSheet.getAttribute('data-testid')).toBe('basic-action-sheet');
});

View File

@@ -105,17 +105,6 @@
&::-ms-clear {
display: none;
}
&::-webkit-date-and-time-value {
/**
* The -webkit-date-and-time-value pseudo element is used
* to style the date/time input on iOS/Mobile Safari.
* To avoid layout shift between an empty state and a selected state,
* we set the height `18px` to match the native input height for
* date/time inputs on iOS/Mobile Safari.
*/
height: 18px;
}
}

View File

@@ -52,18 +52,9 @@
}
.alert-message {
font-size: $alert-md-message-font-size;
}
max-height: $alert-md-content-max-height;
/**
* MD Alerts on tablets can expand vertically up to
* a total maximum height. We only want to set a max-height
* on mobile phones.
*/
@include mobile-viewport() {
.alert-message {
max-height: $alert-md-content-max-height;
}
font-size: $alert-md-message-font-size;
}
.alert-message:empty {
@@ -111,24 +102,14 @@
.alert-checkbox-group {
position: relative;
max-height: $alert-md-content-max-height;
border-top: $alert-md-list-border-top;
border-bottom: $alert-md-list-border-bottom;
overflow: auto;
}
/**
* MD Alerts on tablets can expand vertically up to
* a total maximum height. We only want to set a max-height
* on mobile phones.
*/
@include mobile-viewport() {
.alert-radio-group,
.alert-checkbox-group {
max-height: $alert-md-content-max-height;
}
}
.alert-tappable {
position: relative;
@@ -301,14 +282,3 @@
.alert-button-inner {
justify-content: $alert-md-button-group-justify-content;
}
/**
* MD alerts should scale up to 560px x 560px
* on tablet dimensions.
*/
@include tablet-viewport() {
:host {
--max-width: #{$alert-md-max-width-tablet};
--max-height: #{$alert-md-max-height-tablet};
}
}

View File

@@ -10,20 +10,6 @@ $alert-md-font-size: dynamic-font(14px) !default;
/// @prop - Max width of the alert
$alert-md-max-width: 280px !default;
/// @prop - Max width of the alert on a tablet
/**
* Large display requirements for MD Alert:
* 1. Maintain a minimum of 48px distance from the leading and
* trailing edges of the screen. (48px * 2 = 96px)
* 2. The width can increase up to 560px.
* 3. The height can increase up to 560px.
* Source: https://m2.material.io/components/dialogs#behavior
*/
$alert-md-max-width-tablet: min(calc(100vw - 96px), 560px) !default;
/// @prop - Max width of the alert on a tablet
$alert-md-max-height-tablet: min(calc(100vh - 96px), 560px) !default;
/// @prop - Border radius of the alert
$alert-md-border-radius: 4px !default;

View File

@@ -84,15 +84,7 @@
font-weight: normal;
}
/**
* Alert has a maximum height in scenarios
* such as the MD alert on tablet devices.
* As a result, we need to make sure the inner
* containers can scroll otherwise content
* may be cut off.
*/
.alert-message,
.alert-input-group {
.alert-message {
box-sizing: border-box;
-webkit-overflow-scrolling: touch;
overflow-y: auto;

View File

@@ -1,7 +1,6 @@
import { newSpecPage } from '@stencil/core/testing';
import { config } from '../../../global/config';
import { Alert } from '../alert';
import { config } from '../../../global/config';
describe('alert: custom html', () => {
it('should not allow for custom html by default', async () => {
@@ -10,7 +9,7 @@ describe('alert: custom html', () => {
html: `<ion-alert message="<button class='custom-html'>Custom Text</button>"></ion-alert>`,
});
const content = page.body.querySelector('.alert-message')!;
const content = page.body.querySelector('.alert-message');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).toBe(null);
});
@@ -22,7 +21,7 @@ describe('alert: custom html', () => {
html: `<ion-alert message="<button class='custom-html'>Custom Text</button>"></ion-alert>`,
});
const content = page.body.querySelector('.alert-message')!;
const content = page.body.querySelector('.alert-message');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).not.toBe(null);
});
@@ -34,7 +33,7 @@ describe('alert: custom html', () => {
html: `<ion-alert message="<button class='custom-html'>Custom Text</button>"></ion-alert>`,
});
const content = page.body.querySelector('.alert-message')!;
const content = page.body.querySelector('.alert-message');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).toBe(null);
});

View File

@@ -1,47 +0,0 @@
import { expect } from '@playwright/test';
import { configs, test, Viewports } from '@utils/test/playwright';
/**
* This behavior does not vary across directions.
*/
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('alert: rendering - tablet'), () => {
test.beforeEach(async ({ page }) => {
await page.setViewportSize(Viewports.tablet.portrait);
await page.goto('/src/components/alert/test/basic', config);
});
test('should expand width and height on larger displays with text', async ({ page }) => {
const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent');
const button = page.locator('#longMessage');
const alert = page.locator('ion-alert');
await button.click();
await ionAlertDidPresent.next();
await expect(alert).toHaveScreenshot(screenshot('alert-tablet-text'));
});
test('should expand width and height on larger displays with checkboxes', async ({ page }) => {
const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent');
const button = page.locator('#checkbox');
const alert = page.locator('ion-alert');
await button.click();
await ionAlertDidPresent.next();
await expect(alert).toHaveScreenshot(screenshot('alert-tablet-checkboxes'));
});
test('should expand width and height on larger displays with radios', async ({ page }) => {
const ionAlertDidPresent = await page.spyOnEvent('ionAlertDidPresent');
const button = page.locator('#radio');
const alert = page.locator('ion-alert');
await button.click();
await ionAlertDidPresent.next();
await expect(alert).toHaveScreenshot(screenshot('alert-tablet-radios'));
});
});
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -36,7 +36,7 @@
}
async function showPicker() {
const picker = Object.assign(document.createElement('ion-picker-legacy'), {
const picker = Object.assign(document.createElement('ion-picker'), {
columns: [
{
name: 'Picker',

View File

@@ -1,5 +1,6 @@
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';
@@ -12,6 +13,10 @@ 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.
*/
@@ -32,6 +37,16 @@ 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);

View File

@@ -1,7 +1,7 @@
import { newSpecPage } from '@stencil/core/testing';
import { Breadcrumb } from '../../breadcrumb/breadcrumb';
import { Breadcrumbs } from '../breadcrumbs';
import { Breadcrumb } from '../../breadcrumb/breadcrumb.tsx';
import { Breadcrumbs } from '../breadcrumbs.tsx';
it('should correctly provide the collapsed breadcrumbs in the event payload', async () => {
const page = await newSpecPage({
@@ -18,8 +18,8 @@ it('should correctly provide the collapsed breadcrumbs in the event payload', as
});
const onCollapsedClick = jest.fn((ev) => ev);
const breadcrumbs = page.body.querySelector('ion-breadcrumbs')!;
const breadcrumb = page.body.querySelectorAll('ion-breadcrumb')!;
const breadcrumbs = page.body.querySelector('ion-breadcrumbs');
const breadcrumb = page.body.querySelectorAll('ion-breadcrumb');
breadcrumbs.addEventListener('ionCollapsedClick', onCollapsedClick);
@@ -46,8 +46,8 @@ it('should exclude the separator from narrators', async () => {
`,
});
const firstBreadcrumb = page.body.querySelector('ion-breadcrumb:first-of-type')!;
const separator = firstBreadcrumb.shadowRoot!.querySelector('[part="separator"]')!;
const firstBreadcrumb = page.body.querySelector('ion-breadcrumb:first-of-type');
const separator = firstBreadcrumb.shadowRoot.querySelector('[part="separator"]');
expect(separator.getAttribute('aria-hidden')).toBe('true');
});
@@ -62,7 +62,7 @@ it('should have color attribute', async () => {
`,
});
const breadcrumbs = page.body.querySelector('ion-breadcrumbs')!;
const breadcrumbs = page.body.querySelector('ion-breadcrumbs');
expect(breadcrumbs.hasAttribute('color')).toBe(true);
});

View File

@@ -1,5 +1,4 @@
import { newSpecPage } from '@stencil/core/testing';
import { Button } from '../../button';
describe('Button: Hidden Form Button', () => {
@@ -16,7 +15,8 @@ describe('Button: Hidden Form Button', () => {
return page.body.querySelectorAll('form button');
};
const button = page.body.querySelector('ion-button')!;
const form = page.body.querySelectorAll('form');
const button = page.body.querySelector('ion-button');
await page.waitForChanges();

View File

@@ -11,7 +11,7 @@ describe('ion-checkbox: disabled', () => {
`,
});
const checkbox = page.body.querySelector('ion-checkbox')!;
const checkbox = page.body.querySelector('ion-checkbox');
expect(checkbox.checked).toBe(false);

View File

@@ -54,6 +54,10 @@
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);

View File

@@ -34,24 +34,16 @@
// Calendar / Header / Action Buttons
// -----------------------------------
.calendar-month-year-toggle {
@include padding(0px, 16px, 0px, #{$datetime-ios-padding});
min-height: 44px;
:host .calendar-action-buttons ion-item {
--padding-start: #{$datetime-ios-padding};
--background-hover: transparent;
--background-activated: transparent;
font-size: dynamic-font-max(16px, 1.6);
font-weight: 600;
&.ion-focused::after {
opacity: 0.15;
}
}
.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-item ion-icon,
:host .calendar-action-buttons ion-buttons ion-button {
color: current-color(base);
}

View File

@@ -30,41 +30,15 @@
// 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 {
@@ -90,6 +64,7 @@
* if necessary.
*/
grid-template-rows: repeat(6, 1fr);
}
// Individual day button in month

View File

@@ -34,7 +34,7 @@
* widest item in the column. Setting a minimum
* width avoids this layout shifting.
*/
ion-picker-column {
ion-picker-column-internal {
min-width: 26px;
}
@@ -48,7 +48,7 @@ ion-picker-column {
}
/**
* This ensures that the picker is appropriately
* This ensures that the picker is apppropriately
* sized and never truncates the text.
*/
:host(.datetime-size-fixed.datetime-prefer-wheel) {
@@ -267,8 +267,19 @@ ion-picker-column {
justify-content: space-between;
}
:host .calendar-action-buttons ion-item,
:host .calendar-action-buttons ion-button {
--background: transparent;
--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);
}
// Calendar / Header / Days of Week
@@ -477,55 +488,6 @@ ion-picker-column {
// Year Picker
// -----------------------------------
: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;
:host(.show-month-and-year) .calendar-action-buttons ion-item {
--color: #{current-color(base)};
}

View File

@@ -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/picker-column-interfaces';
import type { PickerColumnItem } from '../picker-column-internal/picker-column-internal-interfaces';
import type {
DatetimePresentation,
@@ -104,6 +104,7 @@ 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[];
@@ -1131,7 +1132,7 @@ export class Datetime implements ComponentInterface {
* so we need to re-init behavior with the new elements.
*/
componentDidRender() {
const { presentation, prevPresentation, calendarBodyRef, minParts, preferWheel, forceRenderDate } = this;
const { presentation, prevPresentation, calendarBodyRef, minParts, preferWheel } = this;
/**
* TODO(FW-2165)
@@ -1149,20 +1150,7 @@ export class Datetime implements ComponentInterface {
const hasCalendarGrid = !preferWheel && ['date-time', 'time-date', 'date'].includes(presentation);
if (minParts !== undefined && hasCalendarGrid && calendarBodyRef) {
const workingMonth = calendarBodyRef.querySelector('.calendar-month:nth-of-type(1)');
/**
* We need to make sure the datetime is not in the process
* of scrolling to a new datetime value if the value
* is updated programmatically.
* Otherwise, the datetime will appear to not scroll at all because
* we are resetting the scroll position to the center of the view.
* Prior to the datetime's value being updated programmatically,
* the calendarBodyRef is scrolled such that the middle month is centered
* in the view. The below code updates the scroll position so the middle
* month is also centered in the view. Since the scroll position did not change,
* the scroll callback in this file does not fire,
* and the resolveForceDateScrolling promise never resolves.
*/
if (workingMonth && forceRenderDate === undefined) {
if (workingMonth) {
calendarBodyRef.scrollLeft = workingMonth.clientWidth * (isRTL(this.el) ? -1 : 1);
}
}
@@ -1527,7 +1515,7 @@ export class Datetime implements ComponentInterface {
forcePresentation === 'time-date'
? [this.renderTimePickerColumns(forcePresentation), this.renderDatePickerColumns(forcePresentation)]
: [this.renderDatePickerColumns(forcePresentation), this.renderTimePickerColumns(forcePresentation)];
return <ion-picker>{renderArray}</ion-picker>;
return <ion-picker-internal>{renderArray}</ion-picker-internal>;
}
private renderDatePickerColumns(forcePresentation: string) {
@@ -1537,7 +1525,7 @@ export class Datetime implements ComponentInterface {
}
private renderCombinedDatePickerColumn() {
const { defaultParts, disabled, workingParts, locale, minParts, maxParts, todayParts, isDateEnabled } = this;
const { defaultParts, workingParts, locale, minParts, maxParts, todayParts, isDateEnabled } = this;
const activePart = this.getActivePartsWithFallback();
@@ -1613,10 +1601,9 @@ export class Datetime implements ComponentInterface {
: `${defaultParts.year}-${defaultParts.month}-${defaultParts.day}`;
return (
<ion-picker-column
<ion-picker-column-internal
class="date-column"
color={this.color}
disabled={disabled}
items={items}
value={todayString}
onIonChange={(ev: CustomEvent) => {
@@ -1647,7 +1634,7 @@ export class Datetime implements ComponentInterface {
ev.stopPropagation();
}}
></ion-picker-column>
></ion-picker-column-internal>
);
}
@@ -1728,15 +1715,14 @@ export class Datetime implements ComponentInterface {
return [];
}
const { disabled, workingParts } = this;
const { workingParts } = this;
const activePart = this.getActivePartsWithFallback();
return (
<ion-picker-column
<ion-picker-column-internal
class="day-column"
color={this.color}
disabled={disabled}
items={days}
value={(workingParts.day !== null ? workingParts.day : this.defaultParts.day) ?? undefined}
onIonChange={(ev: CustomEvent) => {
@@ -1764,7 +1750,7 @@ export class Datetime implements ComponentInterface {
ev.stopPropagation();
}}
></ion-picker-column>
></ion-picker-column-internal>
);
}
@@ -1773,15 +1759,14 @@ export class Datetime implements ComponentInterface {
return [];
}
const { disabled, workingParts } = this;
const { workingParts } = this;
const activePart = this.getActivePartsWithFallback();
return (
<ion-picker-column
<ion-picker-column-internal
class="month-column"
color={this.color}
disabled={disabled}
items={months}
value={workingParts.month}
onIonChange={(ev: CustomEvent) => {
@@ -1809,7 +1794,7 @@ export class Datetime implements ComponentInterface {
ev.stopPropagation();
}}
></ion-picker-column>
></ion-picker-column-internal>
);
}
private renderYearPickerColumn(years: PickerColumnItem[]) {
@@ -1817,15 +1802,14 @@ export class Datetime implements ComponentInterface {
return [];
}
const { disabled, workingParts } = this;
const { workingParts } = this;
const activePart = this.getActivePartsWithFallback();
return (
<ion-picker-column
<ion-picker-column-internal
class="year-column"
color={this.color}
disabled={disabled}
items={years}
value={workingParts.year}
onIonChange={(ev: CustomEvent) => {
@@ -1853,7 +1837,7 @@ export class Datetime implements ComponentInterface {
ev.stopPropagation();
}}
></ion-picker-column>
></ion-picker-column-internal>
);
}
private renderTimePickerColumns(forcePresentation: string) {
@@ -1891,15 +1875,14 @@ export class Datetime implements ComponentInterface {
}
private renderHourPickerColumn(hoursData: PickerColumnItem[]) {
const { disabled, workingParts } = this;
const { workingParts } = this;
if (hoursData.length === 0) return [];
const activePart = this.getActivePartsWithFallback();
return (
<ion-picker-column
<ion-picker-column-internal
color={this.color}
disabled={disabled}
value={activePart.hour}
items={hoursData}
numericInput
@@ -1916,19 +1899,18 @@ export class Datetime implements ComponentInterface {
ev.stopPropagation();
}}
></ion-picker-column>
></ion-picker-column-internal>
);
}
private renderMinutePickerColumn(minutesData: PickerColumnItem[]) {
const { disabled, workingParts } = this;
const { workingParts } = this;
if (minutesData.length === 0) return [];
const activePart = this.getActivePartsWithFallback();
return (
<ion-picker-column
<ion-picker-column-internal
color={this.color}
disabled={disabled}
value={activePart.minute}
items={minutesData}
numericInput
@@ -1945,11 +1927,11 @@ export class Datetime implements ComponentInterface {
ev.stopPropagation();
}}
></ion-picker-column>
></ion-picker-column-internal>
);
}
private renderDayPeriodPickerColumn(dayPeriodData: PickerColumnItem[]) {
const { disabled, workingParts } = this;
const { workingParts } = this;
if (dayPeriodData.length === 0) {
return [];
}
@@ -1958,10 +1940,9 @@ export class Datetime implements ComponentInterface {
const isDayPeriodRTL = isLocaleDayPeriodRTL(this.locale);
return (
<ion-picker-column
<ion-picker-column-internal
style={isDayPeriodRTL ? { order: '-1' } : {}}
color={this.color}
disabled={disabled}
value={activePart.ampm}
items={dayPeriodData}
onIonChange={(ev: CustomEvent) => {
@@ -1981,7 +1962,7 @@ export class Datetime implements ComponentInterface {
ev.stopPropagation();
}}
></ion-picker-column>
></ion-picker-column-internal>
);
}
@@ -2019,18 +2000,35 @@ export class Datetime implements ComponentInterface {
<div class="calendar-header">
<div class="calendar-action-buttons">
<div class="calendar-month-year">
<button
class={{
'calendar-month-year-toggle': true,
'ion-activatable': true,
'ion-focusable': true,
}}
<ion-item
part="month-year-button"
ref={(el) => (this.monthYearToggleItemRef = el)}
button
aria-label="Show year picker"
detail={false}
lines="none"
disabled={disabled}
aria-label={this.showMonthAndYear ? 'Hide year picker' : 'Show year picker'}
onClick={() => this.toggleMonthAndYearView()}
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);
}
}
}}
>
<span id="toggle-wrapper">
<ion-label>
{getMonthAndYear(this.locale, this.workingParts)}
<ion-icon
aria-hidden="true"
@@ -2038,9 +2036,8 @@ export class Datetime implements ComponentInterface {
lazy={false}
flipRtl={true}
></ion-icon>
</span>
{mode === 'md' && <ion-ripple-effect></ion-ripple-effect>}
</button>
</ion-label>
</ion-item>
</div>
<div class="calendar-next-prev">
@@ -2345,7 +2342,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');
const cols = (ev.target! as HTMLElement).querySelectorAll('ion-picker-column-internal');
// TODO (FW-615): Potentially remove this when intersection observers are fixed in picker column
cols.forEach((col) => col.scrollActiveItemIntoView());
}}

View File

@@ -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-toggle');
const monthYearButton = page.locator('.calendar-month-year ion-item');
const prevButton = page.locator('.calendar-next-prev ion-button:nth-child(1)');
const nextButton = page.locator('.calendar-next-prev ion-button:nth-child(2)');

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -1,6 +1,7 @@
import type { SpecPage } from '@stencil/core/testing';
import { newSpecPage } from '@stencil/core/testing';
import { Item } from '../../../item/item';
import { Datetime } from '../../datetime';
describe('datetime', () => {
@@ -19,14 +20,15 @@ describe('datetime', () => {
beforeEach(async () => {
page = await newSpecPage({
components: [Datetime],
components: [Datetime, Item],
html: `<ion-datetime></ion-datetime>`,
});
});
it('should have aria-label "Show year picker" when collapsed', async () => {
const datetime = page.body.querySelector('ion-datetime')!;
const monthYearToggleBtn = datetime.shadowRoot!.querySelector('.calendar-month-year .calendar-month-year-toggle');
const item = datetime.shadowRoot!.querySelector('.calendar-month-year ion-item');
const monthYearToggleBtn = item!.shadowRoot!.querySelector('button');
const ariaLabel = monthYearToggleBtn!.getAttribute('aria-label');
expect(ariaLabel).toContain('Show year picker');
@@ -34,18 +36,15 @@ describe('datetime', () => {
it('should have aria-label "Hide year picker" when expanded', async () => {
const datetime = page.body.querySelector('ion-datetime')!;
const monthYearToggleBtn = datetime.shadowRoot!.querySelector<HTMLButtonElement>(
'.calendar-month-year .calendar-month-year-toggle'
);
const item = datetime.shadowRoot!.querySelector<HTMLIonItemElement>('.calendar-month-year ion-item');
monthYearToggleBtn!.click();
item!.click();
await page.waitForChanges();
const monthYearToggleBtnAfter = datetime.shadowRoot!.querySelector<HTMLButtonElement>(
'.calendar-month-year .calendar-month-year-toggle'
);
const ariaLabel = monthYearToggleBtnAfter!.getAttribute('aria-label');
const itemAfter = datetime.shadowRoot!.querySelector<HTMLIonItemElement>('.calendar-month-year ion-item');
const monthYearToggleBtn = itemAfter!.shadowRoot!.querySelector<HTMLElement>('button');
const ariaLabel = monthYearToggleBtn!.getAttribute('aria-label');
expect(ariaLabel).toContain('Hide year picker');
});

View File

@@ -526,20 +526,4 @@ 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`));
});
});
});

View File

@@ -1,22 +1,21 @@
import type { DatetimeParts } from '../datetime-interface';
import { isSameDay, isBefore, isAfter } from '../utils/comparison';
describe('isSameDay()', () => {
it('should return correct results for month, day, and year', () => {
const reference: DatetimeParts = { month: 1, day: 1, year: 2021 };
const reference = { month: 1, day: 1, year: 2021 };
expect(isSameDay(reference, { month: 1, day: 1, year: 2021 })).toEqual(true);
expect(isSameDay(reference, { month: 2, day: 1, year: 2021 })).toEqual(false);
expect(isSameDay(reference, { month: 1, day: 2, year: 2021 })).toEqual(false);
expect(isSameDay(reference, { month: 1, day: 1, year: 2022 })).toEqual(false);
expect(isSameDay(reference, { month: 0, day: 0, year: 0 })).toEqual(false);
expect(isSameDay(reference, { month: null, day: null, year: null } as any)).toEqual(false);
expect(isSameDay(reference, { month: null, day: null, year: null })).toEqual(false);
});
});
describe('isBefore()', () => {
it('should return correct results for month, day, and year', () => {
const reference: DatetimeParts = { month: 1, day: 1, year: 2021 };
const reference = { month: 1, day: 1, year: 2021 };
expect(isBefore(reference, { month: 1, day: 1, year: 2021 })).toEqual(false);
expect(isBefore(reference, { month: 2, day: 1, year: 2021 })).toEqual(true);
@@ -24,13 +23,13 @@ describe('isBefore()', () => {
expect(isBefore(reference, { month: 1, day: 1, year: 2022 })).toEqual(true);
expect(isBefore(reference, { month: 1, day: 1, year: 2020 })).toEqual(false);
expect(isBefore(reference, { month: 0, day: 0, year: 0 })).toEqual(false);
expect(isBefore(reference, { month: null, day: null, year: null } as any)).toEqual(false);
expect(isBefore(reference, { month: null, day: null, year: null })).toEqual(false);
});
});
describe('isAfter()', () => {
it('should return correct results for month, day, and year', () => {
const reference: DatetimeParts = { month: 2, day: 2, year: 2021 };
const reference = { month: 2, day: 2, year: 2021 };
expect(isAfter(reference, { month: 2, day: 2, year: 2021 })).toEqual(false);
expect(isAfter(reference, { month: 2, day: 1, year: 2021 })).toEqual(true);
@@ -43,6 +42,6 @@ describe('isAfter()', () => {
* 2021 > undefined === false
* 2021 > null === true
*/
expect(isAfter(reference, { month: null, day: null, year: null } as any)).toEqual(true);
expect(isAfter(reference, { month: null, day: null, year: null })).toEqual(true);
});
});

View File

@@ -52,28 +52,28 @@
}
/*
The second selectors that target ion-picker(-column)
The second selectors that target ion-picker(-column)-internal
directly are for styling the time picker. This is currently
undocumented usage.
*/
.custom-grid-wheel,
ion-picker {
ion-picker-internal {
--wheel-highlight-background: rgb(218, 216, 255);
--wheel-fade-background-rgb: 245, 235, 247;
}
ion-picker {
ion-picker-internal {
background-color: rgb(245, 235, 247);
}
.custom-grid-wheel::part(wheel-item),
ion-picker-column::part(wheel-item) {
ion-picker-column-internal::part(wheel-item) {
color: rgb(255, 134, 154);
}
.custom-grid-wheel::part(wheel-item active),
ion-picker-column::part(wheel-item active) {
ion-picker-column-internal::part(wheel-item active) {
color: rgb(128, 30, 171);
}

View File

@@ -1,4 +1,3 @@
import type { DatetimeParts } from '../datetime-interface';
import {
generateMonths,
getDaysOfWeek,
@@ -365,7 +364,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 19,
minute: 50,
} as unknown as DatetimeParts;
};
const minParts = {
day: undefined,
@@ -373,7 +372,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 19,
minute: 50,
} as unknown as DatetimeParts;
};
const { hours } = generateTime('en-US', refValue, 'h23', minParts);
@@ -388,7 +387,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 20,
minute: 22,
} as unknown as DatetimeParts;
};
const minParts = {
day: undefined,
@@ -396,7 +395,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 19,
minute: 30,
} as unknown as DatetimeParts;
};
const { hours, minutes } = generateTime('en-US', refValue, 'h23', minParts);
@@ -412,7 +411,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 20,
minute: 30,
} as unknown as DatetimeParts;
};
const minParts = {
day: undefined,
@@ -420,7 +419,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 19,
minute: 30,
} as unknown as DatetimeParts;
};
const maxParts = {
day: undefined,
@@ -428,7 +427,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 20,
minute: 40,
} as unknown as DatetimeParts;
};
const { hours } = generateTime('en-US', refValue, 'h23', minParts, maxParts);
@@ -442,7 +441,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 13,
minute: 0,
} as unknown as DatetimeParts;
};
const maxParts = {
day: undefined,
@@ -450,7 +449,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 13,
minute: 2,
} as unknown as DatetimeParts;
};
const { minutes } = generateTime('en-US', refValue, 'h23', undefined, maxParts);
@@ -464,7 +463,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 12,
minute: 0,
} as unknown as DatetimeParts;
};
const maxParts = {
day: undefined,
@@ -472,7 +471,7 @@ describe('generateTime()', () => {
year: undefined,
hour: 13,
minute: 2,
} as unknown as DatetimeParts;
};
const { minutes } = generateTime('en-US', refValue, 'h23', undefined, maxParts);
@@ -483,7 +482,7 @@ describe('generateTime()', () => {
describe('getToday', () => {
beforeAll(() => {
jest.useFakeTimers();
jest.useFakeTimers('modern');
// System time is zero based, 1 = February
jest.setSystemTime(new Date(2022, 1, 21, 18, 30));
});

View File

@@ -1,39 +0,0 @@
import { h } from '@stencil/core';
import { newSpecPage } from '@stencil/core/testing';
import { Datetime } from '../../../datetime/datetime';
import { PickerColumn } from '../../../picker-column/picker-column';
import { Picker } from '../../../picker/picker';
describe('ion-datetime disabled', () => {
beforeEach(() => {
// IntersectionObserver isn't available in test environment
const mockIntersectionObserver = jest.fn();
mockIntersectionObserver.mockReturnValue({
observe: () => null,
unobserve: () => null,
disconnect: () => null,
});
global.IntersectionObserver = mockIntersectionObserver;
});
it('picker should be disabled in prefer wheel mode', async () => {
const page = await newSpecPage({
components: [Datetime, PickerColumn, Picker],
template: () => (
<ion-datetime id="inline-datetime-wheel" disabled prefer-wheel value="2022-04-21T00:00:00"></ion-datetime>
),
});
await page.waitForChanges();
const datetime = page.body.querySelector('ion-datetime')!;
const columns = datetime.shadowRoot!.querySelectorAll('ion-picker-column');
await expect(columns.length).toEqual(4);
columns.forEach((column) => {
expect(column.disabled).toBe(true);
});
});
});

View File

@@ -66,11 +66,6 @@
<h2>Inline - No Default Value</h2>
<ion-datetime id="inline-datetime-no-value" disabled></ion-datetime>
</div>
<div class="grid-item">
<h2>Inline - Prefer Wheel</h2>
<ion-datetime id="inline-datetime-wheel" disabled prefer-wheel value="2022-04-21T00:00:00"></ion-datetime>
</div>
</div>
</ion-content>
<script>

View File

@@ -1,4 +1,3 @@
import type { DatetimeParts } from '../datetime-interface';
import {
generateDayAriaLabel,
getMonthAndDay,
@@ -110,7 +109,7 @@ describe('getLocalizedDayPeriod', () => {
describe('getLocalizedTime', () => {
it('should localize the time to PM', () => {
const datetimeParts: DatetimeParts = {
const datetimeParts = {
day: 1,
month: 1,
year: 2022,
@@ -122,7 +121,7 @@ describe('getLocalizedTime', () => {
});
it('should localize the time to AM', () => {
const datetimeParts: DatetimeParts = {
const datetimeParts = {
day: 1,
month: 1,
year: 2022,
@@ -134,7 +133,7 @@ describe('getLocalizedTime', () => {
});
it('should avoid Chromium bug when using 12 hour time in a 24 hour locale', () => {
const datetimeParts: DatetimeParts = {
const datetimeParts = {
day: 1,
month: 1,
year: 2022,
@@ -145,12 +144,12 @@ describe('getLocalizedTime', () => {
expect(getLocalizedTime('en-GB', datetimeParts, 'h12')).toEqual('12:00 am');
});
it('should parse time-only values correctly', () => {
const datetimeParts: Partial<DatetimeParts> = {
const datetimeParts = {
hour: 22,
minute: 40,
};
expect(getLocalizedTime('en-US', datetimeParts as DatetimeParts, 'h12')).toEqual('10:40 PM');
expect(getLocalizedTime('en-US', datetimeParts as DatetimeParts, 'h23')).toEqual('22:40');
expect(getLocalizedTime('en-US', datetimeParts, 'h12')).toEqual('10:40 PM');
expect(getLocalizedTime('en-US', datetimeParts, 'h23')).toEqual('22:40');
});
});

View File

@@ -1,4 +1,3 @@
import type { DatetimeParts } from '../datetime-interface';
import {
getPreviousYear,
getNextYear,
@@ -104,31 +103,31 @@ describe('getInternalHourValue()', () => {
describe('calculateHourFromAMPM()', () => {
it('should correctly convert from AM to PM', () => {
expect(calculateHourFromAMPM({ hour: 12, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(12);
expect(calculateHourFromAMPM({ hour: 1, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(13);
expect(calculateHourFromAMPM({ hour: 2, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(14);
expect(calculateHourFromAMPM({ hour: 3, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(15);
expect(calculateHourFromAMPM({ hour: 4, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(16);
expect(calculateHourFromAMPM({ hour: 5, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(17);
expect(calculateHourFromAMPM({ hour: 6, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(18);
expect(calculateHourFromAMPM({ hour: 7, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(19);
expect(calculateHourFromAMPM({ hour: 8, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(20);
expect(calculateHourFromAMPM({ hour: 9, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(21);
expect(calculateHourFromAMPM({ hour: 10, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(22);
expect(calculateHourFromAMPM({ hour: 11, ampm: 'am' } as DatetimeParts, 'pm')).toEqual(23);
expect(calculateHourFromAMPM({ hour: 12, ampm: 'am' }, 'pm')).toEqual(12);
expect(calculateHourFromAMPM({ hour: 1, ampm: 'am' }, 'pm')).toEqual(13);
expect(calculateHourFromAMPM({ hour: 2, ampm: 'am' }, 'pm')).toEqual(14);
expect(calculateHourFromAMPM({ hour: 3, ampm: 'am' }, 'pm')).toEqual(15);
expect(calculateHourFromAMPM({ hour: 4, ampm: 'am' }, 'pm')).toEqual(16);
expect(calculateHourFromAMPM({ hour: 5, ampm: 'am' }, 'pm')).toEqual(17);
expect(calculateHourFromAMPM({ hour: 6, ampm: 'am' }, 'pm')).toEqual(18);
expect(calculateHourFromAMPM({ hour: 7, ampm: 'am' }, 'pm')).toEqual(19);
expect(calculateHourFromAMPM({ hour: 8, ampm: 'am' }, 'pm')).toEqual(20);
expect(calculateHourFromAMPM({ hour: 9, ampm: 'am' }, 'pm')).toEqual(21);
expect(calculateHourFromAMPM({ hour: 10, ampm: 'am' }, 'pm')).toEqual(22);
expect(calculateHourFromAMPM({ hour: 11, ampm: 'am' }, 'pm')).toEqual(23);
expect(calculateHourFromAMPM({ hour: 13, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(1);
expect(calculateHourFromAMPM({ hour: 14, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(2);
expect(calculateHourFromAMPM({ hour: 15, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(3);
expect(calculateHourFromAMPM({ hour: 16, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(4);
expect(calculateHourFromAMPM({ hour: 17, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(5);
expect(calculateHourFromAMPM({ hour: 18, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(6);
expect(calculateHourFromAMPM({ hour: 19, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(7);
expect(calculateHourFromAMPM({ hour: 20, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(8);
expect(calculateHourFromAMPM({ hour: 21, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(9);
expect(calculateHourFromAMPM({ hour: 22, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(10);
expect(calculateHourFromAMPM({ hour: 23, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(11);
expect(calculateHourFromAMPM({ hour: 0, ampm: 'pm' } as DatetimeParts, 'am')).toEqual(12);
expect(calculateHourFromAMPM({ hour: 13, ampm: 'pm' }, 'am')).toEqual(1);
expect(calculateHourFromAMPM({ hour: 14, ampm: 'pm' }, 'am')).toEqual(2);
expect(calculateHourFromAMPM({ hour: 15, ampm: 'pm' }, 'am')).toEqual(3);
expect(calculateHourFromAMPM({ hour: 16, ampm: 'pm' }, 'am')).toEqual(4);
expect(calculateHourFromAMPM({ hour: 17, ampm: 'pm' }, 'am')).toEqual(5);
expect(calculateHourFromAMPM({ hour: 18, ampm: 'pm' }, 'am')).toEqual(6);
expect(calculateHourFromAMPM({ hour: 19, ampm: 'pm' }, 'am')).toEqual(7);
expect(calculateHourFromAMPM({ hour: 20, ampm: 'pm' }, 'am')).toEqual(8);
expect(calculateHourFromAMPM({ hour: 21, ampm: 'pm' }, 'am')).toEqual(9);
expect(calculateHourFromAMPM({ hour: 22, ampm: 'pm' }, 'am')).toEqual(10);
expect(calculateHourFromAMPM({ hour: 23, ampm: 'pm' }, 'am')).toEqual(11);
expect(calculateHourFromAMPM({ hour: 0, ampm: 'pm' }, 'am')).toEqual(12);
});
});

View File

@@ -109,8 +109,12 @@ 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: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)');
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)'
);
expect(await hours.count()).toBe(12);
expect(await minutes.count()).toBe(60);
@@ -215,7 +219,7 @@ configs({ directions: ['ltr'], modes: ['ios'] }).forEach(({ title, config }) =>
);
const hourPickerItems = page.locator(
'ion-datetime ion-picker-column:first-of-type .picker-item:not(.picker-item-empty)'
'ion-datetime ion-picker-column-internal:first-of-type .picker-item:not(.picker-item-empty)'
);
await expect(hourPickerItems).toHaveText(['8', '9', '10', '11']);
});
@@ -239,7 +243,7 @@ configs({ directions: ['ltr'], modes: ['ios'] }).forEach(({ title, config }) =>
);
const hourPickerItems = page.locator(
'ion-datetime ion-picker-column:first-of-type .picker-item:not(.picker-item-empty)'
'ion-datetime ion-picker-column-internal: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']);
});
@@ -356,7 +360,9 @@ configs({ directions: ['ltr'], modes: ['ios'] }).forEach(({ title, config }) =>
await ionPopoverDidPresent.next();
const hours = page.locator('ion-popover ion-picker-column:nth-child(1) .picker-item:not(.picker-item-empty)');
const hours = page.locator(
'ion-popover ion-picker-column-internal:nth-child(1) .picker-item:not(.picker-item-empty)'
);
await expect(await hours.count()).toBe(4);
});

View File

@@ -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 > .calendar-month-year-toggle');
const pickerButton = page.locator('#date-time .calendar-month-year > ion-item');
await pickerButton.click();
await page.waitForChanges();
await expect(datetimeFooter).not.toBeVisible();

View File

@@ -42,8 +42,7 @@ describe('parseDate()', () => {
* See https://github.com/ionic-team/ionic-framework/commit/3fb4caf21ffac12f765c4c80bf1850e05d211c6a
*/
it('should return the correct time zone offset', () => {
// Casting as any since `tzOffset` does not exist on DatetimeParts
expect((parseDate('2022-12-15T13:47:30-02:00') as any)?.tzOffset).toEqual(undefined);
expect(parseDate('2022-12-15T13:47:30-02:00').tzOffset).toEqual(undefined);
});
it('should parse an array of dates', () => {
@@ -163,8 +162,8 @@ describe('parseMinParts()', () => {
minute: 4,
hour: 2,
};
expect(parseMinParts(undefined as any, today)).toEqual(undefined);
expect(parseMinParts(null as any, today)).toEqual(undefined);
expect(parseMinParts(undefined, today)).toEqual(undefined);
expect(parseMinParts(null, today)).toEqual(undefined);
expect(parseMinParts('foo', today)).toEqual(undefined);
});
});
@@ -226,8 +225,8 @@ describe('parseMaxParts()', () => {
minute: 4,
hour: 2,
};
expect(parseMaxParts(undefined as any, today)).toEqual(undefined);
expect(parseMaxParts(null as any, today)).toEqual(undefined);
expect(parseMaxParts(undefined, today)).toEqual(undefined);
expect(parseMaxParts(null, today)).toEqual(undefined);
expect(parseMaxParts('foo', today)).toEqual(undefined);
});
});

View File

@@ -308,7 +308,7 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
await page.waitForSelector('.datetime-ready');
const columns = page.locator('ion-picker-column');
const columns = page.locator('ion-picker-column-internal');
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');
const columns = page.locator('ion-picker-column-internal');
await expect(columns.nth(0)).toHaveClass(/day-column/);
await expect(columns.nth(1)).toHaveClass(/month-column/);

View File

@@ -227,7 +227,7 @@ class TimePickerFixture {
}
async expectTime(hour: number, minute: number, ampm: string) {
const pickerColumns = this.timePicker.locator('ion-picker-column');
const pickerColumns = this.timePicker.locator('ion-picker-column-internal');
await expect(pickerColumns.nth(0)).toHaveJSProperty('value', hour);
await expect(pickerColumns.nth(1)).toHaveJSProperty('value', minute);

View File

@@ -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-toggle');
const monthYearButton = page.locator('.calendar-month-year ion-item');
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-toggle');
const monthYearButton = page.locator('.calendar-month-year ion-item');
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');

View File

@@ -84,13 +84,13 @@ describe('isPrevMonthDisabled()', () => {
// Date month and year is the same as min month and year
expect(isPrevMonthDisabled({ month: 1, year: 2021, day: null }, { month: 1, year: 2021, day: null })).toEqual(true);
// Date year is the same as min year (month not provided)
expect(
isPrevMonthDisabled({ month: 1, year: 2021, day: null }, { year: 2021, month: null, day: null } as any)
).toEqual(true);
expect(isPrevMonthDisabled({ month: 1, year: 2021, day: null }, { year: 2021, month: null, day: null })).toEqual(
true
);
// Date year is less than the min year (month not provided)
expect(
isPrevMonthDisabled({ month: 5, year: 2021, day: null }, { year: 2022, month: null, day: null } as any)
).toEqual(true);
expect(isPrevMonthDisabled({ month: 5, year: 2021, day: null }, { year: 2022, month: null, day: null })).toEqual(
true
);
// Date is above the maximum bounds and the previous month does not does not fall within the
// min-max range.
@@ -118,12 +118,12 @@ describe('isPrevMonthDisabled()', () => {
expect(isPrevMonthDisabled({ month: 12, year: 2021, day: null })).toEqual(false);
// Date year is the same as min year,
// but can navigate to a previous month without reducing the year.
expect(
isPrevMonthDisabled({ month: 12, year: 2021, day: null }, { year: 2021, month: null, day: null } as any)
).toEqual(false);
expect(
isPrevMonthDisabled({ month: 2, year: 2021, day: null }, { year: 2021, month: null, day: null } as any)
).toEqual(false);
expect(isPrevMonthDisabled({ month: 12, year: 2021, day: null }, { year: 2021, month: null, day: null })).toEqual(
false
);
expect(isPrevMonthDisabled({ month: 2, year: 2021, day: null }, { year: 2021, month: null, day: null })).toEqual(
false
);
});
});

View File

@@ -51,7 +51,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
config
);
const items = page.locator('ion-picker-column:first-of-type .picker-item:not(.picker-item-empty)');
const items = page.locator('ion-picker-column-internal: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:nth-of-type(2) .picker-item:not(.picker-item-empty)');
const items = page.locator('ion-picker-column-internal: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,11 +93,13 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
await page.waitForSelector('.datetime-ready');
const minuteItems = page.locator('ion-picker-column:nth-of-type(2) .picker-item:not(.picker-item-empty)');
const minuteItems = page.locator(
'ion-picker-column-internal: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:nth-of-type(1) .picker-item:not(.picker-item-empty)');
const hourItems = page.locator('ion-picker-column-internal:nth-of-type(1) .picker-item:not(.picker-item-empty)');
await expect(hourItems).toHaveText(['2']);
await expect(hourItems.nth(0)).toHaveClass(/picker-item-active/);
@@ -105,7 +107,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:nth-of-type(3) .picker-item:not(.picker-item-empty)');
const ampmItems = page.locator('ion-picker-column-internal: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/);
});

View File

@@ -1,5 +1,5 @@
import type { Mode } from '../../../interface';
import type { PickerColumnItem } from '../../picker-column/picker-column-interfaces';
import type { PickerColumnItem } from '../../picker-column-internal/picker-column-internal-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
* @returns Date data to be used in ion-picker-column-internal
*/
export const getDayColumnData = (
locale: string,

View File

@@ -1,7 +1,6 @@
import { newSpecPage } from '@stencil/core/testing';
import { config } from '../../../global/config';
import { InfiniteScrollContent } from '../infinite-scroll-content';
import { config } from '../../../global/config';
describe('infinite-scroll-content: custom html', () => {
it('should not allow for custom html by default', async () => {
@@ -10,7 +9,7 @@ describe('infinite-scroll-content: custom html', () => {
html: `<ion-infinite-scroll-content loading-text="<button class='custom-html'>Custom Text</button>"></ion-infinite-scroll-content>`,
});
const content = page.body.querySelector('.infinite-loading-text')!;
const content = page.body.querySelector('.infinite-loading-text');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).toBe(null);
});
@@ -22,7 +21,7 @@ describe('infinite-scroll-content: custom html', () => {
html: `<ion-infinite-scroll-content loading-text="<button class='custom-html'>Custom Text</button>"></ion-infinite-scroll-content>`,
});
const content = page.body.querySelector('.infinite-loading-text')!;
const content = page.body.querySelector('.infinite-loading-text');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).not.toBe(null);
});
@@ -34,7 +33,7 @@ describe('infinite-scroll-content: custom html', () => {
html: `<ion-infinite-scroll-content loading-text="<button class='custom-html'>Custom Text2</button>"></ion-infinite-scroll-content>`,
});
const content = page.body.querySelector('.infinite-loading-text')!;
const content = page.body.querySelector('.infinite-loading-text');
expect(content.textContent).toContain('Custom Text');
expect(content.querySelector('button.custom-html')).toBe(null);
});

View File

@@ -1,5 +1,4 @@
import { newSpecPage } from '@stencil/core/testing';
import { Input } from '../input';
describe('input: rendering', () => {
@@ -9,7 +8,7 @@ describe('input: rendering', () => {
html: '<ion-input title="my title" tabindex="-1" data-form-type="password"></ion-input>',
});
const nativeEl = page.body.querySelector('ion-input input')!;
const nativeEl = page.body.querySelector('ion-input input');
expect(nativeEl.getAttribute('title')).toBe('my title');
expect(nativeEl.getAttribute('tabindex')).toBe('-1');
expect(nativeEl.getAttribute('data-form-type')).toBe('password');
@@ -64,9 +63,9 @@ describe('input: label rendering', () => {
`,
});
const input = page.body.querySelector('ion-input')!;
const input = page.body.querySelector('ion-input');
const labelText = input.querySelector('.label-text-wrapper')!;
const labelText = input.querySelector('.label-text-wrapper');
expect(labelText.textContent).toBe('Label Prop Text');
});
@@ -78,9 +77,9 @@ describe('input: label rendering', () => {
`,
});
const input = page.body.querySelector('ion-input')!;
const input = page.body.querySelector('ion-input');
const labelText = input.querySelector('.label-text-wrapper')!;
const labelText = input.querySelector('.label-text-wrapper');
expect(labelText.textContent).toBe('Label Slot Text');
});
@@ -92,9 +91,9 @@ describe('input: label rendering', () => {
`,
});
const input = page.body.querySelector('ion-input')!;
const input = page.body.querySelector('ion-input');
const labelText = input.querySelector('.label-text-wrapper')!;
const labelText = input.querySelector('.label-text-wrapper');
expect(labelText.textContent).toBe('Label Prop Text');
});

View File

@@ -1,7 +1,6 @@
import { newSpecPage } from '@stencil/core/testing';
import { Item } from '../../../item/item';
import { Input } from '../../input';
import { Item } from '../../../item/item';
it('should render as modern when label is set asynchronously', async () => {
const page = await newSpecPage({
@@ -13,7 +12,7 @@ it('should render as modern when label is set asynchronously', async () => {
`,
});
const input = page.body.querySelector('ion-input')!;
const input = page.body.querySelector('ion-input');
// Template should be modern
expect(input.classList.contains('legacy-input')).toBe(false);

View File

@@ -1,10 +1,9 @@
import { Radio } from '../../../radio/radio.tsx';
import { RadioGroup } from '../../../radio-group/radio-group.tsx';
import { Item } from '../../item.tsx';
import { List } from '../../../list/list.tsx';
import { newSpecPage } from '@stencil/core/testing';
import { List } from '../../../list/list';
import { RadioGroup } from '../../../radio-group/radio-group';
import { Radio } from '../../../radio/radio';
import { Item } from '../../item';
describe('ion-item', () => {
it('should not have a role when used without list', async () => {
const page = await newSpecPage({
@@ -12,7 +11,7 @@ describe('ion-item', () => {
html: `<ion-item>Hello World</ion-item>`,
});
const item = page.body.querySelector('ion-item')!;
const item = page.body.querySelector('ion-item');
expect(item.getAttribute('role')).toBe(null);
});
@@ -28,7 +27,7 @@ describe('ion-item', () => {
`,
});
const item = page.body.querySelector('ion-item')!;
const item = page.body.querySelector('ion-item');
expect(item.getAttribute('role')).toBe('listitem');
});
@@ -46,7 +45,7 @@ describe('ion-item', () => {
`,
});
const item = page.body.querySelector('ion-item')!;
const item = page.body.querySelector('ion-item');
expect(item.getAttribute('role')).toBe(null);
});
});

View File

@@ -7,10 +7,10 @@ describe('loading: htmlAttributes inheritance', () => {
it('should correctly inherit attributes on host', async () => {
const page = await newSpecPage({
components: [Loading],
template: () => <ion-loading overlayIndex={1} htmlAttributes={{ 'data-testid': 'basic-loading' }}></ion-loading>,
template: () => <ion-loading htmlAttributes={{ 'data-testid': 'basic-loading' }}></ion-loading>,
});
const loading = page.body.querySelector('ion-loading')!;
const loading = page.body.querySelector('ion-loading');
await expect(loading.getAttribute('data-testid')).toBe('basic-loading');
});

View File

@@ -52,7 +52,7 @@ export class Menu implements ComponentInterface, MenuI {
width!: number;
_isOpen = false;
backdropEl?: HTMLIonBackdropElement;
backdropEl?: HTMLElement;
menuInnerEl?: HTMLElement;
contentEl?: HTMLElement;
lastFocus?: HTMLElement;

View File

@@ -27,7 +27,7 @@
// iOS Card Modal
// --------------------------------------------------
@include mobile-viewport() {
@media screen and (max-width: 767px) {
@supports (width: max(0px, 1px)) {
:host(.modal-card) {
--height: calc(100% - max(30px, var(--ion-safe-area-top)) - 10px);
@@ -60,7 +60,7 @@
}
}
@include tablet-viewport() {
@media screen and (min-width: 768px) {
:host(.modal-card) {
--width: calc(100% - 120px);
--height: calc(100% - (120px + var(--ion-safe-area-top) + var(--ion-safe-area-bottom)));

View File

@@ -15,8 +15,8 @@ describe('modal: a11y', () => {
`,
});
const modal = page.body.querySelector('ion-modal')!;
const modalWrapper = modal.shadowRoot!.querySelector('.modal-wrapper')!;
const modal = page.body.querySelector('ion-modal');
const modalWrapper = modal.shadowRoot.querySelector('.modal-wrapper');
await expect(modalWrapper.getAttribute('role')).toBe('alertdialog');
});

View File

@@ -7,10 +7,10 @@ describe('modal: htmlAttributes inheritance', () => {
it('should correctly inherit attributes on host', async () => {
const page = await newSpecPage({
components: [Modal],
template: () => <ion-modal htmlAttributes={{ 'data-testid': 'basic-modal' }} overlayIndex={1}></ion-modal>,
template: () => <ion-modal htmlAttributes={{ 'data-testid': 'basic-modal' }}></ion-modal>,
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await expect(modal.getAttribute('data-testid')).toBe('basic-modal');
});

View File

@@ -1,18 +1,19 @@
import { h, setMode } from '@stencil/core';
import { h } from '@stencil/core';
import { newSpecPage } from '@stencil/core/testing';
import { setMode } from '@stencil/core';
import { Content } from '../../../content/content';
import { Modal } from '../../modal';
import { Content } from '../../../content/content';
describe('modal: canDismiss', () => {
describe('modal: regular modal', () => {
it('should dismiss when canDismiss is true', async () => {
const page = await newSpecPage({
components: [Modal],
template: () => <ion-modal overlayIndex={1} animated={false} canDismiss={true}></ion-modal>,
template: () => <ion-modal animated={false} canDismiss={true}></ion-modal>,
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
@@ -25,10 +26,10 @@ describe('modal: canDismiss', () => {
it('should not dismiss when canDismiss is false', async () => {
const page = await newSpecPage({
components: [Modal],
template: () => <ion-modal overlayIndex={1} animated={false} canDismiss={false}></ion-modal>,
template: () => <ion-modal animated={false} canDismiss={false}></ion-modal>,
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
@@ -43,7 +44,6 @@ describe('modal: canDismiss', () => {
components: [Modal],
template: () => (
<ion-modal
overlayIndex={1}
animated={false}
canDismiss={() => {
return new Promise((resolve) => {
@@ -54,7 +54,7 @@ describe('modal: canDismiss', () => {
),
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
@@ -69,7 +69,6 @@ describe('modal: canDismiss', () => {
components: [Modal],
template: () => (
<ion-modal
overlayIndex={1}
animated={false}
canDismiss={() => {
return new Promise((resolve) => {
@@ -80,7 +79,7 @@ describe('modal: canDismiss', () => {
),
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
@@ -95,24 +94,19 @@ describe('modal: canDismiss', () => {
/**
* Card modal is only available on iOS
*/
setMode(() => 'ios');
setMode((elm) => 'ios');
});
it('should dismiss when canDismiss is true', async () => {
const page = await newSpecPage({
components: [Content, Modal],
template: () => (
<ion-modal
overlayIndex={1}
presentingElement={document.createElement('div')}
animated={false}
canDismiss={true}
>
<ion-modal presentingElement={document.createElement('div')} animated={false} canDismiss={true}>
<ion-content>Test Content</ion-content>
</ion-modal>
),
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
@@ -126,18 +120,13 @@ describe('modal: canDismiss', () => {
const page = await newSpecPage({
components: [Content, Modal],
template: () => (
<ion-modal
overlayIndex={1}
presentingElement={document.createElement('div')}
animated={false}
canDismiss={false}
>
<ion-modal presentingElement={document.createElement('div')} animated={false} canDismiss={false}>
<ion-content>Test Content</ion-content>
</ion-modal>
),
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
@@ -152,7 +141,6 @@ describe('modal: canDismiss', () => {
components: [Content, Modal],
template: () => (
<ion-modal
overlayIndex={1}
presentingElement={document.createElement('div')}
animated={false}
canDismiss={() => {
@@ -166,7 +154,7 @@ describe('modal: canDismiss', () => {
),
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
@@ -181,7 +169,6 @@ describe('modal: canDismiss', () => {
components: [Content, Modal],
template: () => (
<ion-modal
overlayIndex={1}
presentingElement={document.createElement('div')}
animated={false}
canDismiss={() => {
@@ -195,7 +182,7 @@ describe('modal: canDismiss', () => {
),
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
@@ -210,17 +197,11 @@ describe('modal: canDismiss', () => {
const page = await newSpecPage({
components: [Modal],
template: () => (
<ion-modal
overlayIndex={1}
breakpoints={[0, 1]}
initialBreakpoint={1}
animated={false}
canDismiss={true}
></ion-modal>
<ion-modal breakpoints={[0, 1]} initialBreakpoint={1} animated={false} canDismiss={true}></ion-modal>
),
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
@@ -234,17 +215,11 @@ describe('modal: canDismiss', () => {
const page = await newSpecPage({
components: [Modal],
template: () => (
<ion-modal
overlayIndex={1}
breakpoints={[0, 1]}
initialBreakpoint={1}
animated={false}
canDismiss={false}
></ion-modal>
<ion-modal breakpoints={[0, 1]} initialBreakpoint={1} animated={false} canDismiss={false}></ion-modal>
),
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
@@ -259,7 +234,6 @@ describe('modal: canDismiss', () => {
components: [Modal],
template: () => (
<ion-modal
overlayIndex={1}
breakpoints={[0, 1]}
initialBreakpoint={1}
animated={false}
@@ -272,7 +246,7 @@ describe('modal: canDismiss', () => {
),
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
@@ -287,7 +261,6 @@ describe('modal: canDismiss', () => {
components: [Modal],
template: () => (
<ion-modal
overlayIndex={1}
breakpoints={[0, 1]}
initialBreakpoint={1}
animated={false}
@@ -300,7 +273,7 @@ describe('modal: canDismiss', () => {
),
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
@@ -315,15 +288,15 @@ describe('modal: canDismiss', () => {
const canDismiss = jest.fn();
const page = await newSpecPage({
components: [Modal],
template: () => <ion-modal overlayIndex={1} animated={false} canDismiss={canDismiss}></ion-modal>,
template: () => <ion-modal animated={false} canDismiss={canDismiss}></ion-modal>,
});
const modal = page.body.querySelector('ion-modal')!;
const modal = page.body.querySelector('ion-modal');
await modal.present();
await page.waitForChanges();
await modal.dismiss('my data', 'my role');
const returnValue = await modal.dismiss('my data', 'my role');
expect(canDismiss).toHaveBeenCalledWith('my data', 'my role');
});

View File

@@ -1,5 +1,6 @@
import { newSpecPage } from '@stencil/core/testing';
import { Config } from '../../../global/config';
import type { ComponentProps } from '../../../interface';
import { Nav } from '../nav';
import type { NavOptions } from '../nav-interface';
@@ -196,6 +197,7 @@ describe('NavController', () => {
.insert(-1, null as any, null, null, trnsDone)
.then(() => {
fail('it should not succeed');
done();
})
.catch((err: Error) => {
const hasCompleted = false;
@@ -250,6 +252,7 @@ describe('NavController', () => {
.pop(null, trnsDone)
.then(() => {
fail('it should not succeed');
done();
})
.catch((err: any) => {
const hasCompleted = false;
@@ -816,10 +819,15 @@ describe('NavController', () => {
beforeEach(async () => {
trnsDone = jest.fn();
const config = new Config();
config.reset({ animated: false });
const page = await newSpecPage({
components: [Nav],
html: `<ion-nav></ion-nav>`,
autoApplyChanges: true,
context: {
config,
},
});
nav = page.rootInstance;
});
@@ -840,7 +848,7 @@ describe('NavController', () => {
pause: jest.fn(),
cancel: jest.fn(),
onfinish: undefined,
} as any;
};
animation.play = () => {
if (animation.onfinish) {

View File

@@ -0,0 +1 @@
@import "./picker-column-internal.scss";

View File

@@ -0,0 +1,6 @@
@import "./picker-column-internal.scss";
@import "../../themes/ionic.globals.md";
:host .picker-item-active {
color: current-color(base);
}

View File

@@ -0,0 +1,94 @@
@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.picker-item-disabled {
scroll-snap-align: none;
cursor: default;
}
:host .picker-item.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);
}
}

View File

@@ -0,0 +1,491 @@
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;
/**
* 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() {
return getElementRoot(this.el).querySelector(
`.picker-item[data-value="${this.value}"]:not([disabled])`
) as HTMLElement | null;
}
render() {
const { items, color, 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}`}
tabindex={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">
&nbsp;
</div>
<div class="picker-item picker-item-empty" aria-hidden="true">
&nbsp;
</div>
<div class="picker-item picker-item-empty" aria-hidden="true">
&nbsp;
</div>
{items.map((item, index) => {
{
/*
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,
'picker-item-disabled': item.disabled || false,
}}
data-value={item.value}
data-index={index}
onClick={(ev: Event) => {
this.centerPickerItemInView(ev.target as HTMLElement, true);
}}
disabled={item.disabled}
part={PICKER_ITEM_PART}
>
{item.text}
</button>
);
})}
<div class="picker-item picker-item-empty" aria-hidden="true">
&nbsp;
</div>
<div class="picker-item picker-item-empty" aria-hidden="true">
&nbsp;
</div>
<div class="picker-item picker-item-empty" aria-hidden="true">
&nbsp;
</div>
</Host>
);
}
}
const PICKER_ITEM_ACTIVE_CLASS = 'picker-item-active';
const PICKER_ITEM_PART = 'wheel-item';
const PICKER_ITEM_ACTIVE_PART = 'active';

View File

@@ -2,7 +2,7 @@
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Picker Column - Basic</title>
<title>Picker Column Internal - 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 - Basic</ion-title>
<ion-title>Picker Column Internal - Basic</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div class="grid">
<div class="grid-item">
<h2>Default</h2>
<ion-picker>
<ion-picker-column id="default"></ion-picker-column>
</ion-picker>
<ion-picker-internal>
<ion-picker-column-internal id="default"></ion-picker-column-internal>
</ion-picker-internal>
</div>
</div>
</ion-content>
@@ -57,18 +57,12 @@
const items = Array(24)
.fill()
.forEach((_, i) => {
const option = document.createElement('ion-picker-column-option');
option.value = i;
option.textContent = i;
.map((_, i) => ({
text: `${i}`,
value: i,
}));
defaultPickerColumn.appendChild(option);
});
//defaultPickerColumn.value = 11;
defaultPickerColumn.addEventListener('ionChange', (ev) => {
console.log(ev);
})
defaultPickerColumn.items = items;
</script>
</ion-app>
</body>

View File

@@ -5,28 +5,33 @@ 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'), () => {
test.describe(title('picker-column-internal'), () => {
test.beforeEach(async ({ page }) => {
await page.goto('/src/components/picker-column/test/basic', config);
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);
});
test('should render 6 empty picker items', async ({ page }) => {
const columns = page.locator('ion-picker-column .picker-item-empty');
const columns = page.locator('ion-picker-column-internal .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 .option-active');
const activeColumn = page.locator('ion-picker-column-internal .picker-item-active');
await expect(activeColumn).toHaveCount(0);
});
test('should have an active item when value is set', async ({ page }) => {
await page.locator('#default').evaluate((el: HTMLIonPickerColumnElement) => {
await page.locator('#default').evaluate((el: HTMLIonPickerColumnInternalElement) => {
el.value = '12';
});
await page.waitForChanges();
const activeColumn = page.locator('ion-picker-column .option-active');
const activeColumn = page.locator('ion-picker-column-internal .picker-item-active');
expect(activeColumn).not.toBeNull();
});
@@ -35,14 +40,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: HTMLIonPickerColumnElement) => {
await page.locator('#default').evaluate((el: HTMLIonPickerColumnInternalElement) => {
el.scrollTop = 801;
});
await page.waitForChanges();
const activeColumn = page.locator('ion-picker-column .option-active');
const activeColumn = page.locator('ion-picker-column-internal .picker-item-active');
await expect(activeColumn).toHaveJSProperty('value', 23);
expect(await activeColumn?.innerText()).toEqual('23');
});
test('should not emit ionChange when the value is modified externally', async ({ page, skip }) => {
@@ -50,7 +55,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
const ionChangeSpy = await page.spyOnEvent('ionChange');
await page.locator('#default').evaluate((el: HTMLIonPickerColumnElement) => {
await page.locator('#default').evaluate((el: HTMLIonPickerColumnInternalElement) => {
el.value = '12';
});
@@ -63,7 +68,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
const ionChangeSpy = await page.spyOnEvent('ionChange');
await page.locator('#default').evaluate((el: HTMLIonPickerColumnElement) => {
await page.locator('#default').evaluate((el: HTMLIonPickerColumnInternalElement) => {
el.scrollTo(0, el.scrollHeight);
});
await page.waitForChanges();

View File

@@ -2,7 +2,7 @@
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8" />
<title>Picker Column Option - Basic</title>
<title>Picker Column Internal - 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,29 +39,33 @@
<ion-app>
<ion-header translucent="true">
<ion-toolbar>
<ion-title>Picker Column Option - Basic</ion-title>
<ion-title>Picker Column Internal - Disabled</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>
<ion-picker-internal>
<ion-picker-column-internal id="default"></ion-picker-column-internal>
</ion-picker-internal>
</div>
</div>
</ion-content>
<script>
const defaultPickerColumn = document.getElementById('default');
const items = Array(24)
.fill()
.map((_, i) => ({
text: `${i}`,
value: i,
disabled: i % 2 === 0,
}));
defaultPickerColumn.items = items;
defaultPickerColumn.value = 12;
</script>
</ion-app>
</body>
</html>

View File

@@ -1,20 +1,50 @@
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: disabled items'), () => {
test.describe(title('picker-column-internal: disabled'), () => {
test('all picker items should be enabled by default', async ({ page }) => {
await page.setContent(
`
<ion-picker>
<ion-picker-column></ion-picker-column>
</ion-picker>
<ion-picker-internal>
<ion-picker-column-internal></ion-picker-column-internal>
</ion-picker-internal>
<script>
const column = document.querySelector('ion-picker-column');
const column = document.querySelector('ion-picker-column-internal');
column.items = [
{ text: 'A', value: 'a' },
{ text: 'B', value: 'b' },
@@ -25,19 +55,21 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
config
);
const pickerItems = page.locator('ion-picker-column .picker-item:not(.picker-item-empty, [disabled])');
const pickerItems = page.locator(
'ion-picker-column-internal .picker-item:not(.picker-item-empty, .picker-item-disabled)'
);
expect(await pickerItems.count()).toBe(3);
});
test('disabled picker item should not be interactive', async ({ page }) => {
await page.setContent(
`
<ion-picker>
<ion-picker-column></ion-picker-column>
</ion-picker>
<ion-picker-internal>
<ion-picker-column-internal></ion-picker-column-internal>
</ion-picker-internal>
<script>
const column = document.querySelector('ion-picker-column');
const column = document.querySelector('ion-picker-column-internal');
column.items = [
{ text: 'A', value: 'a' },
{ text: 'B', value: 'b', disabled: true },
@@ -48,18 +80,18 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
config
);
const disabledItem = page.locator('ion-picker-column .picker-item[disabled]');
const disabledItem = page.locator('ion-picker-column-internal .picker-item.picker-item-disabled');
await expect(disabledItem).not.toBeEnabled();
});
test('disabled picker item should not be considered active', async ({ page }) => {
await page.setContent(
`
<ion-picker>
<ion-picker-column value="b"></ion-picker-column>
</ion-picker>
<ion-picker-internal>
<ion-picker-column-internal value="b"></ion-picker-column-internal>
</ion-picker-internal>
<script>
const column = document.querySelector('ion-picker-column');
const column = document.querySelector('ion-picker-column-internal');
column.items = [
{ text: 'A', value: 'a' },
{ text: 'B', value: 'b', disabled: true },
@@ -70,18 +102,18 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
config
);
const disabledItem = page.locator('ion-picker-column .picker-item[data-value="b"]');
const disabledItem = page.locator('ion-picker-column-internal .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>
<ion-picker-column></ion-picker-column>
</ion-picker>
<ion-picker-internal>
<ion-picker-column-internal></ion-picker-column-internal>
</ion-picker-internal>
<script>
const column = document.querySelector('ion-picker-column');
const column = document.querySelector('ion-picker-column-internal');
column.items = [
{ text: 'A', value: 'a' },
{ text: 'B', value: 'b', disabled: true },
@@ -92,24 +124,24 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
config
);
const pickerColumn = page.locator('ion-picker-column');
await pickerColumn.evaluate((el: HTMLIonPickerColumnElement) => (el.value = 'b'));
const pickerColumn = page.locator('ion-picker-column-internal');
await pickerColumn.evaluate((el: HTMLIonPickerColumnInternalElement) => (el.value = 'b'));
await page.waitForChanges();
const disabledItem = page.locator('ion-picker-column .picker-item[data-value="b"]');
await expect(disabledItem).toBeDisabled();
const disabledItem = page.locator('ion-picker-column-internal .picker-item[data-value="b"]');
await expect(disabledItem).toHaveClass(/picker-item-disabled/);
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>
<ion-picker-column></ion-picker-column>
</ion-picker>
<ion-picker-internal>
<ion-picker-column-internal></ion-picker-column-internal>
</ion-picker-internal>
<script>
const column = document.querySelector('ion-picker-column');
const column = document.querySelector('ion-picker-column-internal');
column.items = [
{ text: 'A', value: 'a' },
{ text: 'B', value: 'b', disabled: true },
@@ -121,43 +153,9 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
config
);
const disabledItem = page.locator('ion-picker-column .picker-item[data-value="b"]');
await expect(disabledItem).toBeDisabled();
const disabledItem = page.locator('ion-picker-column-internal .picker-item[data-value="b"]');
await expect(disabledItem).toHaveClass(/picker-item-disabled/);
await expect(disabledItem).not.toHaveClass(/picker-item-active/);
});
});
});
/**
* This behavior does not vary across directions.
*/
configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
test.describe(title('picker-column: disabled column rendering'), () => {
test.beforeEach(async ({ page }) => {
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-disabled-column'));
});
});
});
/**
* This behavior does not vary across modes/directions.
*/
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
test.describe(title('picker-column: disabled column'), () => {
test.beforeEach(async ({ page }) => {
await page.goto('/src/components/picker-column/test/disabled', config);
});
test('item in disabled column should not be interactive', async ({ page }) => {
const secondItem = page.locator('#column-disabled .picker-item:not(.picker-item-empty)').nth(1);
await expect(secondItem).toBeDisabled();
});
});
});

View File

@@ -1 +0,0 @@
@import "./picker-column-option.scss";

View File

@@ -1,5 +0,0 @@
@import "./picker-column-option.scss";
:host(.option-active) button {
color: current-color(base);
}

View File

@@ -1,45 +0,0 @@
@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;
}

View File

@@ -1,111 +0,0 @@
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>
);
}
}

View File

@@ -1,22 +0,0 @@
<!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>

Some files were not shown because too many files have changed in this diff Show More