Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d841b4225 | ||
|
|
ee6ba200d1 | ||
|
|
7ce1031c17 | ||
|
|
516b84475e | ||
|
|
bc51dd05cf | ||
|
|
ae6c353b51 | ||
|
|
ec07e70d99 | ||
|
|
fb23bf74d5 | ||
|
|
f6531fa2be | ||
|
|
150ea1a8ad | ||
|
|
92f1b8627a | ||
|
|
8ee23d20d5 | ||
|
|
8f66acdffb | ||
|
|
6f9ee6024b | ||
|
|
dc51b33db5 | ||
|
|
a17b963182 | ||
|
|
8f7d87c680 | ||
|
|
fc88613fef | ||
|
|
e886e3ff2f | ||
|
|
37290df7ef | ||
|
|
c680ad9988 | ||
|
|
ffa3073211 | ||
|
|
ec9228f54f | ||
|
|
f3799f332b |
9
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,4 +1,4 @@
|
||||
Issue number: #
|
||||
Issue number: resolves #
|
||||
|
||||
---------
|
||||
|
||||
@@ -21,7 +21,12 @@ Issue number: #
|
||||
- [ ] Yes
|
||||
- [ ] No
|
||||
|
||||
<!-- If this introduces a breaking change, please describe the impact and migration path for existing applications below. -->
|
||||
<!--
|
||||
If this introduces a breaking change:
|
||||
1. Describe the impact and migration path for existing applications below.
|
||||
2. Update the BREAKING.md file with the breaking change.
|
||||
3. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/.github/CONTRIBUTING.md#footer for more information.
|
||||
-->
|
||||
|
||||
|
||||
## Other information
|
||||
|
||||
31
CHANGELOG.md
@@ -3,6 +3,35 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [7.6.2](https://github.com/ionic-team/ionic-framework/compare/v7.6.1...v7.6.2) (2023-12-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **input, textarea, select:** reduce padding on slotted buttons ([#28676](https://github.com/ionic-team/ionic-framework/issues/28676)) ([516b844](https://github.com/ionic-team/ionic-framework/commit/516b84475e5d78060f35fa2c4821efc712536353))
|
||||
* **item:** label does not expand indefinitely ([#28700](https://github.com/ionic-team/ionic-framework/issues/28700)) ([bc51dd0](https://github.com/ionic-team/ionic-framework/commit/bc51dd05cf036656980de584d2367db46054f774))
|
||||
* **refresher:** mode property can be used in typescript ([#28717](https://github.com/ionic-team/ionic-framework/issues/28717)) ([7ce1031](https://github.com/ionic-team/ionic-framework/commit/7ce1031c177487649c2a698664ec98f10d9002b9)), closes [#28716](https://github.com/ionic-team/ionic-framework/issues/28716)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [7.6.1](https://github.com/ionic-team/ionic-framework/compare/v7.6.0...v7.6.1) (2023-12-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **datetime:** prefer wheel sets working value on confirmation ([#28520](https://github.com/ionic-team/ionic-framework/issues/28520)) ([e886e3f](https://github.com/ionic-team/ionic-framework/commit/e886e3ff2fcb8a3586a62881c5fc848f3074235d)), closes [#25839](https://github.com/ionic-team/ionic-framework/issues/25839)
|
||||
* **input, textarea:** clearOnInput ignores key modifiers ([#28639](https://github.com/ionic-team/ionic-framework/issues/28639)) ([8f7d87c](https://github.com/ionic-team/ionic-framework/commit/8f7d87c6803b1600a3ca21785df0e9bac49f74a3)), closes [#28633](https://github.com/ionic-team/ionic-framework/issues/28633)
|
||||
* **menu:** allow styling of the box shadow and transform when visible inside of a split pane ([#28691](https://github.com/ionic-team/ionic-framework/issues/28691)) ([8ee23d2](https://github.com/ionic-team/ionic-framework/commit/8ee23d20d5cc7419ce15f047b92d2f826d3eb681)), closes [#21530](https://github.com/ionic-team/ionic-framework/issues/21530)
|
||||
* **react:** avoid type collision with @types/react@18.2.43 and greater ([#28687](https://github.com/ionic-team/ionic-framework/issues/28687)) ([92f1b86](https://github.com/ionic-team/ionic-framework/commit/92f1b8627a240c93891205f75adcb5ce3d46596d))
|
||||
* **react:** replacing route uses new route direction and animation ([#28671](https://github.com/ionic-team/ionic-framework/issues/28671)) ([a17b963](https://github.com/ionic-team/ionic-framework/commit/a17b9631829c36c2daf1d5227f5afa69f99f8743)), closes [#24260](https://github.com/ionic-team/ionic-framework/issues/24260)
|
||||
* **react:** use custom animation when going back after a replace ([#28674](https://github.com/ionic-team/ionic-framework/issues/28674)) ([fc88613](https://github.com/ionic-team/ionic-framework/commit/fc88613fefa019a3b695a2c6e10c85cd3ce79ae8)), closes [#28673](https://github.com/ionic-team/ionic-framework/issues/28673)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [7.6.0](https://github.com/ionic-team/ionic-framework/compare/v7.5.8...v7.6.0) (2023-12-06)
|
||||
|
||||
|
||||
@@ -28,6 +57,8 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
|
||||
* **toast:** add swipe to dismiss functionality ([#28442](https://github.com/ionic-team/ionic-framework/issues/28442)) ([30c21aa](https://github.com/ionic-team/ionic-framework/commit/30c21aab3ed40d73c28e7d60d0952d8891b0a9d3)), closes [#21769](https://github.com/ionic-team/ionic-framework/issues/21769)
|
||||
* **toggle:** expose label wrapper as shadow part ([#28585](https://github.com/ionic-team/ionic-framework/issues/28585)) ([a34188f](https://github.com/ionic-team/ionic-framework/commit/a34188f7dbec4a16e4f2043ed3dc096e337725a7))
|
||||
|
||||
Note: Text inside of `ion-item` can now wrap to resolve accessibility issues related to readability. We recommend evaluating your application to account for text wrapping.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,32 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [7.6.2](https://github.com/ionic-team/ionic-framework/compare/v7.6.1...v7.6.2) (2023-12-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **input, textarea, select:** reduce padding on slotted buttons ([#28676](https://github.com/ionic-team/ionic-framework/issues/28676)) ([516b844](https://github.com/ionic-team/ionic-framework/commit/516b84475e5d78060f35fa2c4821efc712536353))
|
||||
* **item:** label does not expand indefinitely ([#28700](https://github.com/ionic-team/ionic-framework/issues/28700)) ([bc51dd0](https://github.com/ionic-team/ionic-framework/commit/bc51dd05cf036656980de584d2367db46054f774))
|
||||
* **refresher:** mode property can be used in typescript ([#28717](https://github.com/ionic-team/ionic-framework/issues/28717)) ([7ce1031](https://github.com/ionic-team/ionic-framework/commit/7ce1031c177487649c2a698664ec98f10d9002b9)), closes [#28716](https://github.com/ionic-team/ionic-framework/issues/28716)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [7.6.1](https://github.com/ionic-team/ionic-framework/compare/v7.6.0...v7.6.1) (2023-12-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **datetime:** prefer wheel sets working value on confirmation ([#28520](https://github.com/ionic-team/ionic-framework/issues/28520)) ([e886e3f](https://github.com/ionic-team/ionic-framework/commit/e886e3ff2fcb8a3586a62881c5fc848f3074235d)), closes [#25839](https://github.com/ionic-team/ionic-framework/issues/25839)
|
||||
* **input, textarea:** clearOnInput ignores key modifiers ([#28639](https://github.com/ionic-team/ionic-framework/issues/28639)) ([8f7d87c](https://github.com/ionic-team/ionic-framework/commit/8f7d87c6803b1600a3ca21785df0e9bac49f74a3)), closes [#28633](https://github.com/ionic-team/ionic-framework/issues/28633)
|
||||
* **menu:** allow styling of the box shadow and transform when visible inside of a split pane ([#28691](https://github.com/ionic-team/ionic-framework/issues/28691)) ([8ee23d2](https://github.com/ionic-team/ionic-framework/commit/8ee23d20d5cc7419ce15f047b92d2f826d3eb681)), closes [#21530](https://github.com/ionic-team/ionic-framework/issues/21530)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [7.6.0](https://github.com/ionic-team/ionic-framework/compare/v7.5.8...v7.6.0) (2023-12-06)
|
||||
|
||||
|
||||
|
||||
@@ -1087,6 +1087,7 @@ ion-range,part,tick-active
|
||||
ion-refresher,none
|
||||
ion-refresher,prop,closeDuration,string,'280ms',false,false
|
||||
ion-refresher,prop,disabled,boolean,false,false,false
|
||||
ion-refresher,prop,mode,"ios" | "md",undefined,false,false
|
||||
ion-refresher,prop,pullFactor,number,1,false,false
|
||||
ion-refresher,prop,pullMax,number,this.pullMin + 60,false,false
|
||||
ion-refresher,prop,pullMin,number,60,false,false
|
||||
|
||||
46
core/package-lock.json
generated
@@ -1,23 +1,23 @@
|
||||
{
|
||||
"name": "@ionic/core",
|
||||
"version": "7.6.0",
|
||||
"version": "7.6.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@ionic/core",
|
||||
"version": "7.6.0",
|
||||
"version": "7.6.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stencil/core": "^4.8.1",
|
||||
"@stencil/core": "^4.8.2",
|
||||
"ionicons": "^7.2.1",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.8.1",
|
||||
"@axe-core/playwright": "^4.8.2",
|
||||
"@capacitor/core": "^5.5.1",
|
||||
"@capacitor/haptics": "^5.0.6",
|
||||
"@capacitor/keyboard": "^5.0.6",
|
||||
"@capacitor/keyboard": "^5.0.7",
|
||||
"@capacitor/status-bar": "^5.0.6",
|
||||
"@ionic/eslint-config": "^0.3.0",
|
||||
"@ionic/prettier-config": "^2.0.0",
|
||||
@@ -56,9 +56,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@axe-core/playwright": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.8.1.tgz",
|
||||
"integrity": "sha512-KC1X++UdRAwMLRvB+BIKFheyLHUnbJTL0t0Wbv6TJMozn2V2QyEtAcN6jyUiudtGiLUGhHCtj/eWorBnVZ4dAA==",
|
||||
"version": "4.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.8.2.tgz",
|
||||
"integrity": "sha512-9KOhX2tNuvqn9DzpBNyqoqNKRZBrexeSiN9irQ0sEdq8zH13JnatepCJxobuXn4UopNy6iIpP4342beMiH+MSQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"axe-core": "~4.8.2"
|
||||
@@ -652,9 +652,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/keyboard": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-5.0.6.tgz",
|
||||
"integrity": "sha512-9GewAa/y2Hwkdw/Be8MTdiAjrFZ7TPDKpR44M0Y/0QMnK+mBbgzcoZ/UkuumWv6e2F1IAI+VY5eYVQHDeZcRoA==",
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-5.0.7.tgz",
|
||||
"integrity": "sha512-+6lW8z2nXTM2NOG7D7pOasCfIGicz26+EeDRXIj5AtJibbjwtE1Q5GIY+qGHgzpmwOF0qmcrGJBz4zagDwUapg==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": "^5.0.0"
|
||||
@@ -1825,9 +1825,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@stencil/core": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.8.1.tgz",
|
||||
"integrity": "sha512-KG1H10j24rlyxIqOI4CG8/h9T7ObTv7giW2H3u1qXV4KKrLykDOpMcLzpqNXqL2Fki3s1QvHyl/oaRvi5waWVw==",
|
||||
"version": "4.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.8.2.tgz",
|
||||
"integrity": "sha512-KdZEAtz9VnqMtXOkf51+8mphyRt0fN/LYgtj5M8gnveGspG8KzoyTDzlWt0wsstWIsJJ21RA1yd3AgMMZiu3MA==",
|
||||
"bin": {
|
||||
"stencil": "bin/stencil"
|
||||
},
|
||||
@@ -10899,9 +10899,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@axe-core/playwright": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.8.1.tgz",
|
||||
"integrity": "sha512-KC1X++UdRAwMLRvB+BIKFheyLHUnbJTL0t0Wbv6TJMozn2V2QyEtAcN6jyUiudtGiLUGhHCtj/eWorBnVZ4dAA==",
|
||||
"version": "4.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.8.2.tgz",
|
||||
"integrity": "sha512-9KOhX2tNuvqn9DzpBNyqoqNKRZBrexeSiN9irQ0sEdq8zH13JnatepCJxobuXn4UopNy6iIpP4342beMiH+MSQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"axe-core": "~4.8.2"
|
||||
@@ -11340,9 +11340,9 @@
|
||||
"requires": {}
|
||||
},
|
||||
"@capacitor/keyboard": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-5.0.6.tgz",
|
||||
"integrity": "sha512-9GewAa/y2Hwkdw/Be8MTdiAjrFZ7TPDKpR44M0Y/0QMnK+mBbgzcoZ/UkuumWv6e2F1IAI+VY5eYVQHDeZcRoA==",
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-5.0.7.tgz",
|
||||
"integrity": "sha512-+6lW8z2nXTM2NOG7D7pOasCfIGicz26+EeDRXIj5AtJibbjwtE1Q5GIY+qGHgzpmwOF0qmcrGJBz4zagDwUapg==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
@@ -12184,9 +12184,9 @@
|
||||
"requires": {}
|
||||
},
|
||||
"@stencil/core": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.8.1.tgz",
|
||||
"integrity": "sha512-KG1H10j24rlyxIqOI4CG8/h9T7ObTv7giW2H3u1qXV4KKrLykDOpMcLzpqNXqL2Fki3s1QvHyl/oaRvi5waWVw=="
|
||||
"version": "4.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.8.2.tgz",
|
||||
"integrity": "sha512-KdZEAtz9VnqMtXOkf51+8mphyRt0fN/LYgtj5M8gnveGspG8KzoyTDzlWt0wsstWIsJJ21RA1yd3AgMMZiu3MA=="
|
||||
},
|
||||
"@stencil/react-output-target": {
|
||||
"version": "0.5.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ionic/core",
|
||||
"version": "7.6.0",
|
||||
"version": "7.6.2",
|
||||
"description": "Base components for Ionic",
|
||||
"keywords": [
|
||||
"ionic",
|
||||
@@ -31,15 +31,15 @@
|
||||
"loader/"
|
||||
],
|
||||
"dependencies": {
|
||||
"@stencil/core": "^4.8.1",
|
||||
"@stencil/core": "^4.8.2",
|
||||
"ionicons": "^7.2.1",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.8.1",
|
||||
"@axe-core/playwright": "^4.8.2",
|
||||
"@capacitor/core": "^5.5.1",
|
||||
"@capacitor/haptics": "^5.0.6",
|
||||
"@capacitor/keyboard": "^5.0.6",
|
||||
"@capacitor/keyboard": "^5.0.7",
|
||||
"@capacitor/status-bar": "^5.0.6",
|
||||
"@ionic/eslint-config": "^0.3.0",
|
||||
"@ionic/prettier-config": "^2.0.0",
|
||||
|
||||
8
core/src/components.d.ts
vendored
@@ -2377,6 +2377,10 @@ export namespace Components {
|
||||
* A number representing how far down the user has pulled. The number `0` represents the user hasn't pulled down at all. The number `1`, and anything greater than `1`, represents that the user has pulled far enough down that when they let go then the refresh will happen. If they let go and the number is less than `1`, then the refresh will not happen, and the content will return to it's original position.
|
||||
*/
|
||||
"getProgress": () => Promise<number>;
|
||||
/**
|
||||
* The mode determines which platform styles to use.
|
||||
*/
|
||||
"mode"?: "ios" | "md";
|
||||
/**
|
||||
* How much to multiply the pull speed by. To slow the pull animation down, pass a number less than `1`. To speed up the pull, pass a number greater than `1`. The default value is `1` which is equal to the speed of the cursor. If a negative value is passed in, the factor will be `1` instead. For example: If the value passed is `1.2` and the content is dragged by `10` pixels, instead of `10` pixels the content will be pulled by `12` pixels (an increase of 20 percent). If the value passed is `0.8`, the dragged amount will be `8` pixels, less than the amount the cursor has moved. Does not apply when the refresher content uses a spinner, enabling the native refresher.
|
||||
*/
|
||||
@@ -7089,6 +7093,10 @@ declare namespace LocalJSX {
|
||||
* If `true`, the refresher will be hidden.
|
||||
*/
|
||||
"disabled"?: boolean;
|
||||
/**
|
||||
* The mode determines which platform styles to use.
|
||||
*/
|
||||
"mode"?: "ios" | "md";
|
||||
/**
|
||||
* Emitted while the user is pulling down the content and exposing the refresher.
|
||||
*/
|
||||
|
||||
@@ -492,7 +492,7 @@ export class Datetime implements ComponentInterface {
|
||||
*/
|
||||
@Method()
|
||||
async confirm(closeOverlay = false) {
|
||||
const { isCalendarPicker, activeParts } = this;
|
||||
const { isCalendarPicker, activeParts, preferWheel, workingParts } = this;
|
||||
|
||||
/**
|
||||
* We only update the value if the presentation is not a calendar picker.
|
||||
@@ -500,7 +500,16 @@ export class Datetime implements ComponentInterface {
|
||||
if (activeParts !== undefined || !isCalendarPicker) {
|
||||
const activePartsIsArray = Array.isArray(activeParts);
|
||||
if (activePartsIsArray && activeParts.length === 0) {
|
||||
this.setValue(undefined);
|
||||
if (preferWheel) {
|
||||
/**
|
||||
* If the datetime is using a wheel picker, but the
|
||||
* active parts are empty, then the user has confirmed the
|
||||
* initial value (working parts) presented to them.
|
||||
*/
|
||||
this.setValue(convertDataToISO(workingParts));
|
||||
} else {
|
||||
this.setValue(undefined);
|
||||
}
|
||||
} else {
|
||||
this.setValue(convertDataToISO(activeParts));
|
||||
}
|
||||
@@ -1356,11 +1365,21 @@ export class Datetime implements ComponentInterface {
|
||||
const dayValues = (this.parsedDayValues = convertToArrayOfNumbers(this.dayValues));
|
||||
|
||||
const todayParts = (this.todayParts = parseDate(getToday())!);
|
||||
this.defaultParts = getClosestValidDate(todayParts, monthValues, dayValues, yearValues, hourValues, minuteValues);
|
||||
|
||||
this.processMinParts();
|
||||
this.processMaxParts();
|
||||
|
||||
this.defaultParts = getClosestValidDate({
|
||||
refParts: todayParts,
|
||||
monthValues,
|
||||
dayValues,
|
||||
yearValues,
|
||||
hourValues,
|
||||
minuteValues,
|
||||
minParts: this.minParts,
|
||||
maxParts: this.maxParts,
|
||||
});
|
||||
|
||||
this.processValue(this.value);
|
||||
|
||||
this.emitStyle();
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
subtractDays,
|
||||
addDays,
|
||||
validateParts,
|
||||
getClosestValidDate,
|
||||
} from '../utils/manipulation';
|
||||
|
||||
describe('addDays()', () => {
|
||||
@@ -558,3 +559,160 @@ describe('validateParts()', () => {
|
||||
).toEqual({ month: 1, day: 1, year: 2022, hour: 9, minute: 30 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getClosestValidDate()', () => {
|
||||
it('should match a date with only month/day/year', () => {
|
||||
// October 10, 2023
|
||||
const refParts = { month: 10, day: 10, year: 2023 };
|
||||
// April 10, 2021
|
||||
const minParts = { month: 4, day: 10, year: 2021 };
|
||||
// September 14, 2021
|
||||
const maxParts = { month: 9, day: 14, year: 2021 };
|
||||
|
||||
// September 4, 2021
|
||||
const expected = { month: 9, day: 4, year: 2021, dayOfWeek: undefined };
|
||||
|
||||
expect(
|
||||
getClosestValidDate({
|
||||
refParts,
|
||||
monthValues: [2, 3, 7, 9, 10],
|
||||
dayValues: [4, 15, 25],
|
||||
yearValues: [2020, 2021, 2023],
|
||||
maxParts,
|
||||
minParts,
|
||||
})
|
||||
).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should match a date when the reference date is before the min', () => {
|
||||
// April 2, 2020 3:20 PM
|
||||
const refParts = { month: 4, day: 2, year: 2020, hour: 15, minute: 20 };
|
||||
// September 10, 2021 10:10 AM
|
||||
const minParts = { month: 9, day: 10, year: 2021, hour: 10, minute: 10 };
|
||||
// September 14, 2021 10:11 AM
|
||||
const maxParts = { month: 9, day: 14, year: 2021, hour: 10, minute: 11 };
|
||||
|
||||
// September 11, 2021 11:15 AM
|
||||
const expected = {
|
||||
year: 2021,
|
||||
day: 11,
|
||||
month: 9,
|
||||
hour: 11,
|
||||
minute: 15,
|
||||
ampm: 'am',
|
||||
dayOfWeek: undefined,
|
||||
};
|
||||
|
||||
expect(
|
||||
getClosestValidDate({
|
||||
refParts,
|
||||
monthValues: [4, 9, 11],
|
||||
dayValues: [11, 12, 13, 14],
|
||||
yearValues: [2020, 2021, 2023],
|
||||
hourValues: [9, 10, 11],
|
||||
minuteValues: [11, 12, 13, 14, 15],
|
||||
maxParts,
|
||||
minParts,
|
||||
})
|
||||
).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should match a date when the reference date is before the min', () => {
|
||||
// April 2, 2020 3:20 PM
|
||||
const refParts = { month: 4, day: 2, year: 2020, hour: 15, minute: 20 };
|
||||
// September 10, 2021 10:10 AM
|
||||
const minParts = { month: 9, day: 10, year: 2021, hour: 10, minute: 10 };
|
||||
// September 10, 2021 10:15 AM
|
||||
const maxParts = { month: 9, day: 10, year: 2021, hour: 10, minute: 15 };
|
||||
|
||||
// September 10, 2021 10:15 AM
|
||||
const expected = {
|
||||
month: 9,
|
||||
day: 10,
|
||||
year: 2021,
|
||||
hour: 10,
|
||||
minute: 15,
|
||||
ampm: 'am',
|
||||
dayOfWeek: undefined,
|
||||
};
|
||||
|
||||
expect(
|
||||
getClosestValidDate({
|
||||
refParts,
|
||||
monthValues: [4, 9, 11],
|
||||
dayValues: [10, 12, 13, 14],
|
||||
yearValues: [2020, 2021, 2023],
|
||||
hourValues: [9, 10, 11],
|
||||
minuteValues: [11, 12, 13, 14, 15],
|
||||
minParts,
|
||||
maxParts,
|
||||
})
|
||||
).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should only clamp minutes if within the same day and hour as min/max', () => {
|
||||
// April 2, 2020 9:16 AM
|
||||
const refParts = { month: 4, day: 2, year: 2020, hour: 9, minute: 16 };
|
||||
// September 10, 2021 10:10 AM
|
||||
const minParts = { month: 9, day: 10, year: 2021, hour: 10, minute: 10 };
|
||||
// September 10, 2021 11:15 AM
|
||||
const maxParts = { month: 9, day: 10, year: 2021, hour: 11, minute: 15 };
|
||||
|
||||
// September 10, 2021 10:16 AM
|
||||
const expected = {
|
||||
month: 9,
|
||||
day: 10,
|
||||
year: 2021,
|
||||
hour: 10,
|
||||
minute: 16,
|
||||
ampm: 'am',
|
||||
dayOfWeek: undefined,
|
||||
};
|
||||
|
||||
expect(
|
||||
getClosestValidDate({
|
||||
refParts,
|
||||
monthValues: [4, 9, 11],
|
||||
dayValues: [10, 12, 13, 14],
|
||||
yearValues: [2020, 2021, 2023],
|
||||
hourValues: [9, 10, 11],
|
||||
minuteValues: [10, 15, 16],
|
||||
minParts,
|
||||
maxParts,
|
||||
})
|
||||
).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should return the closest valid date after adjusting the allowed year', () => {
|
||||
// April 2, 2022 9:16 AM
|
||||
const refParts = { month: 4, day: 2, year: 2022, hour: 9, minute: 16 };
|
||||
// September 10, 2021 10:10 AM
|
||||
const minParts = { month: 9, day: 10, year: 2021, hour: 10, minute: 10 };
|
||||
// September 10, 2023 11:15 AM
|
||||
const maxParts = { month: 9, day: 10, year: 2023, hour: 11, minute: 15 };
|
||||
|
||||
// April 2, 2022 9:16 AM
|
||||
const expected = {
|
||||
month: 4,
|
||||
day: 2,
|
||||
year: 2022,
|
||||
hour: 9,
|
||||
minute: 16,
|
||||
ampm: 'am',
|
||||
dayOfWeek: undefined,
|
||||
};
|
||||
|
||||
expect(
|
||||
getClosestValidDate({
|
||||
refParts,
|
||||
monthValues: [4, 9, 11],
|
||||
dayValues: [2, 10, 12, 13, 14],
|
||||
yearValues: [2020, 2021, 2022, 2023],
|
||||
hourValues: [9, 10, 11],
|
||||
minuteValues: [10, 15, 16],
|
||||
minParts,
|
||||
maxParts,
|
||||
})
|
||||
).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { newSpecPage } from '@stencil/core/testing';
|
||||
|
||||
import { Datetime } from '../../datetime';
|
||||
|
||||
describe('datetime: preferWheel', () => {
|
||||
beforeEach(() => {
|
||||
const mockIntersectionObserver = jest.fn();
|
||||
mockIntersectionObserver.mockReturnValue({
|
||||
observe: () => null,
|
||||
unobserve: () => null,
|
||||
disconnect: () => null,
|
||||
});
|
||||
global.IntersectionObserver = mockIntersectionObserver;
|
||||
});
|
||||
|
||||
it('should select the working day when clicking the confirm button', async () => {
|
||||
const page = await newSpecPage({
|
||||
components: [Datetime],
|
||||
html: '<ion-datetime prefer-wheel="true" max="2021" show-default-buttons="true"></ion-datetime>',
|
||||
});
|
||||
|
||||
const datetime = page.body.querySelector<HTMLIonDatetimeElement>('ion-datetime')!;
|
||||
const confirmButton = datetime.shadowRoot!.querySelector<HTMLIonButtonElement>('#confirm-button')!;
|
||||
|
||||
confirmButton.click();
|
||||
|
||||
await page.waitForChanges();
|
||||
|
||||
expect(datetime.value).toBe('2021-12-31T23:59:00');
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DatetimeParts } from '../datetime-interface';
|
||||
|
||||
import { isSameDay } from './comparison';
|
||||
import { isAfter, isBefore, isSameDay } from './comparison';
|
||||
import { getNumDaysInMonth } from './helpers';
|
||||
import { clampDate, parseAmPm } from './parse';
|
||||
|
||||
@@ -424,44 +424,137 @@ export const validateParts = (
|
||||
* Returns the closest date to refParts
|
||||
* that also meets the constraints of
|
||||
* the *Values params.
|
||||
* @param refParts The reference date
|
||||
* @param monthValues The allowed month values
|
||||
* @param dayValues The allowed day (of the month) values
|
||||
* @param yearValues The allowed year values
|
||||
* @param hourValues The allowed hour values
|
||||
* @param minuteValues The allowed minute values
|
||||
*/
|
||||
export const getClosestValidDate = (
|
||||
refParts: DatetimeParts,
|
||||
monthValues?: number[],
|
||||
dayValues?: number[],
|
||||
yearValues?: number[],
|
||||
hourValues?: number[],
|
||||
minuteValues?: number[]
|
||||
) => {
|
||||
export const getClosestValidDate = ({
|
||||
refParts,
|
||||
monthValues,
|
||||
dayValues,
|
||||
yearValues,
|
||||
hourValues,
|
||||
minuteValues,
|
||||
minParts,
|
||||
maxParts,
|
||||
}: {
|
||||
/**
|
||||
* The reference date
|
||||
*/
|
||||
refParts: DatetimeParts;
|
||||
/**
|
||||
* The allowed month values
|
||||
*/
|
||||
monthValues?: number[];
|
||||
/**
|
||||
* The allowed day (of the month) values
|
||||
*/
|
||||
dayValues?: number[];
|
||||
/**
|
||||
* The allowed year values
|
||||
*/
|
||||
yearValues?: number[];
|
||||
/**
|
||||
* The allowed hour values
|
||||
*/
|
||||
hourValues?: number[];
|
||||
/**
|
||||
* The allowed minute values
|
||||
*/
|
||||
minuteValues?: number[];
|
||||
/**
|
||||
* The minimum date that can be returned
|
||||
*/
|
||||
minParts?: DatetimeParts;
|
||||
/**
|
||||
* The maximum date that can be returned
|
||||
*/
|
||||
maxParts?: DatetimeParts;
|
||||
}) => {
|
||||
const { hour, minute, day, month, year } = refParts;
|
||||
const copyParts = { ...refParts, dayOfWeek: undefined };
|
||||
|
||||
if (yearValues !== undefined) {
|
||||
// Filters out years that are out of the min/max bounds
|
||||
const filteredYears = yearValues.filter((year) => {
|
||||
if (minParts !== undefined && year < minParts.year) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (maxParts !== undefined && year > maxParts.year) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
copyParts.year = findClosestValue(year, filteredYears);
|
||||
}
|
||||
|
||||
if (monthValues !== undefined) {
|
||||
copyParts.month = findClosestValue(month, monthValues);
|
||||
// Filters out months that are out of the min/max bounds
|
||||
const filteredMonths = monthValues.filter((month) => {
|
||||
if (minParts !== undefined && copyParts.year === minParts.year && month < minParts.month) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (maxParts !== undefined && copyParts.year === maxParts.year && month > maxParts.month) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
copyParts.month = findClosestValue(month, filteredMonths);
|
||||
}
|
||||
|
||||
// Day is nullable but cannot be undefined
|
||||
if (day !== null && dayValues !== undefined) {
|
||||
copyParts.day = findClosestValue(day, dayValues);
|
||||
}
|
||||
|
||||
if (yearValues !== undefined) {
|
||||
copyParts.year = findClosestValue(year, yearValues);
|
||||
// Filters out days that are out of the min/max bounds
|
||||
const filteredDays = dayValues.filter((day) => {
|
||||
if (minParts !== undefined && isBefore({ ...copyParts, day }, minParts)) {
|
||||
return false;
|
||||
}
|
||||
if (maxParts !== undefined && isAfter({ ...copyParts, day }, maxParts)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
copyParts.day = findClosestValue(day, filteredDays);
|
||||
}
|
||||
|
||||
if (hour !== undefined && hourValues !== undefined) {
|
||||
copyParts.hour = findClosestValue(hour, hourValues);
|
||||
// Filters out hours that are out of the min/max bounds
|
||||
const filteredHours = hourValues.filter((hour) => {
|
||||
if (minParts?.hour !== undefined && isSameDay(copyParts, minParts) && hour < minParts.hour) {
|
||||
return false;
|
||||
}
|
||||
if (maxParts?.hour !== undefined && isSameDay(copyParts, maxParts) && hour > maxParts.hour) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
copyParts.hour = findClosestValue(hour, filteredHours);
|
||||
copyParts.ampm = parseAmPm(copyParts.hour);
|
||||
}
|
||||
|
||||
if (minute !== undefined && minuteValues !== undefined) {
|
||||
copyParts.minute = findClosestValue(minute, minuteValues);
|
||||
// Filters out minutes that are out of the min/max bounds
|
||||
const filteredMinutes = minuteValues.filter((minute) => {
|
||||
if (
|
||||
minParts?.minute !== undefined &&
|
||||
isSameDay(copyParts, minParts) &&
|
||||
copyParts.hour === minParts.hour &&
|
||||
minute < minParts.minute
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
maxParts?.minute !== undefined &&
|
||||
isSameDay(copyParts, maxParts) &&
|
||||
copyParts.hour === maxParts.hour &&
|
||||
minute > maxParts.minute
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
copyParts.minute = findClosestValue(minute, filteredMinutes);
|
||||
}
|
||||
|
||||
return copyParts;
|
||||
|
||||
@@ -37,3 +37,25 @@
|
||||
:host(.input-disabled) {
|
||||
opacity: #{$input-ios-disabled-opacity};
|
||||
}
|
||||
|
||||
// Start/End Slots
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Slotted buttons have a lot of default padding that can
|
||||
* cause them to look misaligned from other pieces such
|
||||
* as the control's label, especially when using a clear
|
||||
* fill. We also make them circular to ensure that non-
|
||||
* clear buttons and the focus/hover state on clear ones
|
||||
* don't look too crowded.
|
||||
*/
|
||||
::slotted(ion-button[slot="start"].button-has-icon-only),
|
||||
::slotted(ion-button[slot="end"].button-has-icon-only) {
|
||||
--border-radius: 50%;
|
||||
--padding-start: 0;
|
||||
--padding-end: 0;
|
||||
--padding-top: 0;
|
||||
--padding-bottom: 0;
|
||||
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
@@ -117,3 +117,27 @@
|
||||
:host(.input-shape-round) {
|
||||
--border-radius: 16px;
|
||||
}
|
||||
|
||||
// Start/End Slots
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Slotted buttons have a lot of default padding that can
|
||||
* cause them to look misaligned from other pieces such
|
||||
* as the control's label, especially when using a clear
|
||||
* fill. We also make them circular to ensure that non-
|
||||
* clear buttons and the focus/hover state on clear ones
|
||||
* don't look too crowded.
|
||||
*/
|
||||
::slotted(ion-button[slot="start"].button-has-icon-only),
|
||||
::slotted(ion-button[slot="end"].button-has-icon-only) {
|
||||
--border-radius: 50%;
|
||||
--padding-start: 8px;
|
||||
--padding-end: 8px;
|
||||
--padding-top: 8px;
|
||||
--padding-bottom: 8px;
|
||||
|
||||
aspect-ratio: 1;
|
||||
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
@@ -659,8 +659,10 @@
|
||||
|
||||
::slotted([slot="start"]) {
|
||||
margin-inline-end: $form-control-label-margin;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
::slotted([slot="end"]) {
|
||||
margin-inline-start: $form-control-label-margin;
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
@@ -549,15 +549,37 @@ export class Input implements ComponentInterface {
|
||||
if (!this.shouldClearOnEdit()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* The following keys do not modify the
|
||||
* contents of the input. As a result, pressing
|
||||
* them should not edit the input.
|
||||
*
|
||||
* We can't check to see if the value of the input
|
||||
* was changed because we call checkClearOnEdit
|
||||
* in a keydown listener, and the key has not yet
|
||||
* been added to the input.
|
||||
*/
|
||||
const IGNORED_KEYS = ['Enter', 'Tab', 'Shift', 'Meta', 'Alt', 'Control'];
|
||||
const pressedIgnoredKey = IGNORED_KEYS.includes(ev.key);
|
||||
|
||||
/**
|
||||
* Clear the input if the control has not been previously cleared during focus.
|
||||
* Do not clear if the user hitting enter to submit a form.
|
||||
*/
|
||||
if (!this.didInputClearOnEdit && this.hasValue() && ev.key !== 'Enter' && ev.key !== 'Tab') {
|
||||
if (!this.didInputClearOnEdit && this.hasValue() && !pressedIgnoredKey) {
|
||||
this.value = '';
|
||||
this.emitInputChange(ev);
|
||||
}
|
||||
this.didInputClearOnEdit = true;
|
||||
|
||||
/**
|
||||
* Pressing an IGNORED_KEYS first and
|
||||
* then an allowed key will cause the input to not
|
||||
* be cleared.
|
||||
*/
|
||||
if (!pressedIgnoredKey) {
|
||||
this.didInputClearOnEdit = true;
|
||||
}
|
||||
}
|
||||
|
||||
private onCompositionStart = () => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test, configs } from '@utils/test/playwright';
|
||||
|
||||
const IGNORED_KEYS = ['Enter', 'Tab', 'Shift', 'Meta', 'Alt', 'Control'];
|
||||
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('input: clearOnEdit'), () => {
|
||||
test('should clear when typed into', async ({ page }) => {
|
||||
@@ -16,28 +18,57 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
await expect(input).toHaveJSProperty('value', 'h');
|
||||
});
|
||||
|
||||
test('should not clear when enter is pressed', async ({ page }) => {
|
||||
await page.setContent(`<ion-input value="abc" clear-on-edit="true" aria-label="input"></ion-input>`, config);
|
||||
test('should not clear the input if it does not have an initial value when typing', async ({ page }) => {
|
||||
await page.setContent(`<ion-input label="input" value="" clear-on-edit="true"></ion-input>`, config);
|
||||
|
||||
const input = page.locator('ion-input');
|
||||
await input.locator('input').focus();
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForChanges();
|
||||
await input.click();
|
||||
await input.type('hello world');
|
||||
|
||||
await expect(input).toHaveJSProperty('value', 'abc');
|
||||
await expect(input).toHaveJSProperty('value', 'hello world');
|
||||
});
|
||||
|
||||
test('should not clear when tab is pressed', async ({ page }) => {
|
||||
IGNORED_KEYS.forEach((ignoredKey: string) => {
|
||||
test(`should not clear when ${ignoredKey} is pressed`, async ({ page, skip }) => {
|
||||
skip.browser(
|
||||
(browserName: string) => browserName === 'firefox' && ignoredKey === 'Meta',
|
||||
'Firefox incorrectly adds "OS" to the input when pressing the Meta key on Linux'
|
||||
);
|
||||
await page.setContent(`<ion-input value="abc" clear-on-edit="true" aria-label="input"></ion-input>`, config);
|
||||
|
||||
const input = page.locator('ion-input');
|
||||
await input.locator('input').focus();
|
||||
|
||||
await page.keyboard.press(ignoredKey);
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(input).toHaveJSProperty('value', 'abc');
|
||||
});
|
||||
});
|
||||
|
||||
test('should clear after when pressing valid key after pressing ignored key', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/28633',
|
||||
});
|
||||
|
||||
await page.setContent(`<ion-input value="abc" clear-on-edit="true" aria-label="input"></ion-input>`, config);
|
||||
|
||||
const input = page.locator('ion-input');
|
||||
await input.locator('input').focus();
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
// ignored
|
||||
await page.keyboard.press('Shift');
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(input).toHaveJSProperty('value', 'abc');
|
||||
|
||||
// allowed
|
||||
await page.keyboard.press('a');
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(input).toHaveJSProperty('value', 'a');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,9 @@ configs().forEach(({ title, screenshot, config }) => {
|
||||
`
|
||||
<ion-input label-placement="start" fill="solid" value="100" label="Weight" clear-input="true">
|
||||
<ion-icon slot="start" name="barbell" aria-hidden="true"></ion-icon>
|
||||
<ion-label slot="end">lbs</ion-label>
|
||||
<ion-button slot="end" aria-label="Show/hide password">
|
||||
<ion-icon slot="icon-only" name="lock-closed" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-input>
|
||||
`,
|
||||
config
|
||||
@@ -23,7 +25,9 @@ configs().forEach(({ title, screenshot, config }) => {
|
||||
`
|
||||
<ion-input label-placement="floating" fill="solid" value="100" label="Weight" clear-input="true">
|
||||
<ion-icon slot="start" name="barbell" aria-hidden="true"></ion-icon>
|
||||
<ion-label slot="end">lbs</ion-label>
|
||||
<ion-button slot="end" aria-label="Show/hide password">
|
||||
<ion-icon slot="icon-only" name="lock-closed" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-input>
|
||||
`,
|
||||
config
|
||||
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 4.0 KiB |
@@ -426,6 +426,13 @@ button, a {
|
||||
// shrink and wrap its text instead of moving to its
|
||||
// own row if there are slotted elements next to it
|
||||
width: min-content;
|
||||
|
||||
/**
|
||||
* We allow labels in the default
|
||||
* slot to grow. However, we do not
|
||||
* want them to grow indefinitely.
|
||||
*/
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
// Item Input
|
||||
|
||||
@@ -82,3 +82,34 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
|
||||
test.describe(title('label: text wrapping in item'), () => {
|
||||
test('long text should not cause label to expand infinitely', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
div {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<!-- This text should be truncated with ellipses -->
|
||||
<div>
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
</div>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const item = page.locator('ion-item');
|
||||
|
||||
await expect(item).toHaveScreenshot(screenshot(`label-item-wrap`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
@@ -190,13 +190,12 @@ ion-backdrop {
|
||||
|
||||
width: auto;
|
||||
|
||||
/* stylelint-disable declaration-no-important */
|
||||
transform: none !important;
|
||||
transform: none;
|
||||
|
||||
box-shadow: none !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:host(.menu-pane-visible) ion-backdrop {
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
display: hidden !important;
|
||||
/* stylelint-enable declaration-no-important */
|
||||
}
|
||||
|
||||
65
core/src/components/menu/test/custom/menu.e2e.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* This behavior does not vary across directions.
|
||||
*/
|
||||
configs({ directions: ['ltr'] }).forEach(({ title, config, screenshot }) => {
|
||||
test.describe(title('menu: custom'), () => {
|
||||
test('should allow styling the menu box shadow when inside a split pane', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/21530',
|
||||
});
|
||||
|
||||
await page.setContent(
|
||||
`
|
||||
<style>
|
||||
ion-split-pane {
|
||||
--side-width: 200px;
|
||||
--side-min-width: 200px;
|
||||
}
|
||||
|
||||
ion-menu {
|
||||
box-shadow: 10px 5px 5px red;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ion-app>
|
||||
<ion-split-pane when="xs" id="splitPane" content-id="split-content">
|
||||
<ion-menu side="start" content-id="split-content">
|
||||
<ion-header>
|
||||
<ion-toolbar color="secondary">
|
||||
<ion-title>Menu</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">Menu Content</ion-content>
|
||||
</ion-menu>
|
||||
|
||||
<div class="ion-page" id="split-content">
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-menu-button></ion-menu-button>
|
||||
</ion-buttons>
|
||||
|
||||
<ion-title>Content</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">Main Content</ion-content>
|
||||
</div>
|
||||
</ion-split-pane>
|
||||
</ion-app>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const app = page.locator('ion-app');
|
||||
|
||||
await expect(app).toHaveScreenshot(screenshot(`menu-custom-split-pane`));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
@@ -15,7 +15,8 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
});
|
||||
});
|
||||
|
||||
test.describe(title('radio: keyboard navigation'), () => {
|
||||
// TODO(FW-5715): re-enable test
|
||||
test.skip(title('radio: keyboard navigation'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
|
||||
@@ -25,6 +25,9 @@ import {
|
||||
translateElement,
|
||||
} from './refresher.utils';
|
||||
|
||||
/**
|
||||
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
|
||||
*/
|
||||
@Component({
|
||||
tag: 'ion-refresher',
|
||||
styleUrls: {
|
||||
|
||||
@@ -38,3 +38,25 @@
|
||||
:host(.select-disabled) {
|
||||
opacity: $select-ios-disabled-opacity;
|
||||
}
|
||||
|
||||
// Start/End Slots
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Slotted buttons have a lot of default padding that can
|
||||
* cause them to look misaligned from other pieces such
|
||||
* as the control's label, especially when using a clear
|
||||
* fill. We also make them circular to ensure that non-
|
||||
* clear buttons and the focus/hover state on clear ones
|
||||
* don't look too crowded.
|
||||
*/
|
||||
::slotted(ion-button[slot="start"].button-has-icon-only),
|
||||
::slotted(ion-button[slot="end"].button-has-icon-only) {
|
||||
--border-radius: 50%;
|
||||
--padding-start: 0;
|
||||
--padding-end: 0;
|
||||
--padding-top: 0;
|
||||
--padding-bottom: 0;
|
||||
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
@@ -155,3 +155,27 @@
|
||||
:host(.select-disabled) {
|
||||
opacity: $select-md-disabled-opacity;
|
||||
}
|
||||
|
||||
// Start/End Slots
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Slotted buttons have a lot of default padding that can
|
||||
* cause them to look misaligned from other pieces such
|
||||
* as the control's label, especially when using a clear
|
||||
* fill. We also make them circular to ensure that non-
|
||||
* clear buttons and the focus/hover state on clear ones
|
||||
* don't look too crowded.
|
||||
*/
|
||||
::slotted(ion-button[slot="start"].button-has-icon-only),
|
||||
::slotted(ion-button[slot="end"].button-has-icon-only) {
|
||||
--border-radius: 50%;
|
||||
--padding-start: 8px;
|
||||
--padding-end: 8px;
|
||||
--padding-top: 8px;
|
||||
--padding-bottom: 8px;
|
||||
|
||||
aspect-ratio: 1;
|
||||
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
@@ -592,8 +592,10 @@ button {
|
||||
|
||||
::slotted([slot="start"]) {
|
||||
margin-inline-end: $form-control-label-margin;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
::slotted([slot="end"]) {
|
||||
margin-inline-start: $form-control-label-margin;
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ configs().forEach(({ title, screenshot, config }) => {
|
||||
`
|
||||
<ion-select label-placement="start" fill="solid" placeholder="Select weight" label="Weight">
|
||||
<ion-icon slot="start" name="barbell" aria-hidden="true"></ion-icon>
|
||||
<ion-label slot="end">lbs</ion-label>
|
||||
<ion-button slot="end" aria-label="Show/hide password">
|
||||
<ion-icon slot="icon-only" name="lock-closed" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-select>
|
||||
`,
|
||||
config
|
||||
@@ -23,7 +25,9 @@ configs().forEach(({ title, screenshot, config }) => {
|
||||
`
|
||||
<ion-select label-placement="floating" fill="solid" placeholder="Select weight" label="Weight">
|
||||
<ion-icon slot="start" name="barbell" aria-hidden="true"></ion-icon>
|
||||
<ion-label slot="end">lbs</ion-label>
|
||||
<ion-button slot="end" aria-label="Show/hide password">
|
||||
<ion-icon slot="icon-only" name="lock-closed" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-select>
|
||||
`,
|
||||
config
|
||||
|
||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 5.0 KiB |
@@ -43,8 +43,7 @@
|
||||
|
||||
position: relative;
|
||||
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
box-shadow: none !important;
|
||||
box-shadow: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { test, configs } from '@utils/test/playwright';
|
||||
|
||||
const IGNORED_KEYS = ['Tab', 'Shift', 'Meta', 'Alt', 'Control'];
|
||||
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('textarea: clearOnEdit'), () => {
|
||||
test('should clear when typed into', async ({ page }) => {
|
||||
@@ -33,5 +35,64 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
|
||||
|
||||
await expect(textarea).toHaveJSProperty('value', 'abc');
|
||||
});
|
||||
|
||||
test('should not clear the textarea if it does not have an initial value when typing', async ({ page }) => {
|
||||
await page.setContent(`<ion-textarea label="textarea" value="" clear-on-edit="true"></ion-textarea>`, config);
|
||||
|
||||
const textarea = page.locator('ion-textarea');
|
||||
|
||||
await textarea.click();
|
||||
await textarea.type('hello world');
|
||||
|
||||
await expect(textarea).toHaveJSProperty('value', 'hello world');
|
||||
});
|
||||
|
||||
IGNORED_KEYS.forEach((ignoredKey: string) => {
|
||||
test(`should not clear when ${ignoredKey} is pressed`, async ({ page, skip }) => {
|
||||
skip.browser(
|
||||
(browserName: string) => browserName === 'firefox' && ignoredKey === 'Meta',
|
||||
'Firefox incorrectly adds "OS" to the textarea when pressing the Meta key on Linux'
|
||||
);
|
||||
await page.setContent(
|
||||
`<ion-textarea value="abc" clear-on-edit="true" aria-label="textarea"></ion-textarea>`,
|
||||
config
|
||||
);
|
||||
|
||||
const textarea = page.locator('ion-textarea');
|
||||
await textarea.locator('textarea').focus();
|
||||
|
||||
await page.keyboard.press(ignoredKey);
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(textarea).toHaveJSProperty('value', 'abc');
|
||||
});
|
||||
});
|
||||
|
||||
test('should clear after when pressing valid key after pressing ignored key', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/28633',
|
||||
});
|
||||
|
||||
await page.setContent(
|
||||
`<ion-textarea value="abc" clear-on-edit="true" aria-label="textarea"></ion-textarea>`,
|
||||
config
|
||||
);
|
||||
|
||||
const textarea = page.locator('ion-textarea');
|
||||
await textarea.locator('textarea').focus();
|
||||
|
||||
// ignored
|
||||
await page.keyboard.press('Shift');
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(textarea).toHaveJSProperty('value', 'abc');
|
||||
|
||||
// allowed
|
||||
await page.keyboard.press('a');
|
||||
await page.waitForChanges();
|
||||
|
||||
await expect(textarea).toHaveJSProperty('value', 'a');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,9 @@ configs().forEach(({ title, screenshot, config }) => {
|
||||
`
|
||||
<ion-textarea label-placement="start" fill="solid" value="100" label="Weight">
|
||||
<ion-icon slot="start" name="barbell" aria-hidden="true"></ion-icon>
|
||||
<ion-label slot="end">lbs</ion-label>
|
||||
<ion-button slot="end" aria-label="Show/hide password">
|
||||
<ion-icon slot="icon-only" name="lock-closed" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-textarea>
|
||||
`,
|
||||
config
|
||||
@@ -23,7 +25,9 @@ configs().forEach(({ title, screenshot, config }) => {
|
||||
`
|
||||
<ion-textarea label-placement="floating" fill="solid" value="100" label="Weight">
|
||||
<ion-icon slot="start" name="barbell" aria-hidden="true"></ion-icon>
|
||||
<ion-label slot="end">lbs</ion-label>
|
||||
<ion-button slot="end" aria-label="Show/hide password">
|
||||
<ion-icon slot="icon-only" name="lock-closed" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-textarea>
|
||||
`,
|
||||
config
|
||||
|
||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.6 KiB |