@ionic/core 7.3.5-dev.11694621267.1e5f63c2 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/components/button-active.js +1 -1
  2. package/components/checkbox.js +6 -3
  3. package/components/data.js +31 -7
  4. package/components/index.js +2 -2
  5. package/components/index2.js +203 -429
  6. package/components/index3.js +429 -203
  7. package/components/ion-datetime-button.js +3 -0
  8. package/components/ion-datetime.js +132 -92
  9. package/components/ion-item-sliding.js +1 -1
  10. package/components/ion-menu.js +1 -1
  11. package/components/ion-modal.js +2 -2
  12. package/components/ion-nav.js +1 -1
  13. package/components/ion-range.js +3 -3
  14. package/components/ion-refresher.js +3 -3
  15. package/components/ion-reorder-group.js +1 -1
  16. package/components/ion-router-outlet.js +1 -1
  17. package/components/ion-segment.js +1 -1
  18. package/components/ion-toggle.js +7 -4
  19. package/components/ios.transition.js +1 -1
  20. package/components/md.transition.js +1 -1
  21. package/components/notch-controller.js +1 -1
  22. package/components/picker-column.js +1 -1
  23. package/components/popover.js +1 -1
  24. package/components/radio.js +6 -3
  25. package/components/swipe-back.js +1 -1
  26. package/css/core.css.map +1 -1
  27. package/css/ionic.bundle.css.map +1 -1
  28. package/dist/cjs/{data-967b9c15.js → data-db832785.js} +31 -7
  29. package/dist/cjs/index.cjs.js +10 -9
  30. package/dist/cjs/ion-checkbox.cjs.entry.js +5 -3
  31. package/dist/cjs/ion-datetime-button.cjs.entry.js +4 -1
  32. package/dist/cjs/ion-datetime_3.cjs.entry.js +132 -93
  33. package/dist/cjs/ion-input.cjs.entry.js +1 -1
  34. package/dist/cjs/ion-radio_2.cjs.entry.js +5 -3
  35. package/dist/cjs/ion-range.cjs.entry.js +2 -2
  36. package/dist/cjs/ion-select_3.cjs.entry.js +1 -1
  37. package/dist/cjs/ion-textarea.cjs.entry.js +1 -1
  38. package/dist/cjs/ion-toggle.cjs.entry.js +5 -3
  39. package/dist/cjs/ionic.cjs.js +1 -1
  40. package/dist/cjs/loader.cjs.js +1 -1
  41. package/dist/cjs/{notch-controller-c27fe0b7.js → notch-controller-f4f6af5d.js} +1 -1
  42. package/dist/collection/components/checkbox/checkbox.ios.css +67 -0
  43. package/dist/collection/components/checkbox/checkbox.js +24 -4
  44. package/dist/collection/components/checkbox/checkbox.md.css +67 -0
  45. package/dist/collection/components/checkbox/test/item/checkbox.e2e.js +13 -0
  46. package/dist/collection/components/checkbox/test/label/checkbox.e2e.js +27 -0
  47. package/dist/collection/components/datetime/datetime.ios.css +32 -29
  48. package/dist/collection/components/datetime/datetime.js +137 -93
  49. package/dist/collection/components/datetime/datetime.md.css +20 -28
  50. package/dist/collection/components/datetime/test/basic/datetime.e2e.js +25 -0
  51. package/dist/collection/components/datetime/test/color/datetime.e2e.js +1 -1
  52. package/dist/collection/components/datetime/test/custom/datetime.e2e.js +27 -1
  53. package/dist/collection/components/datetime/test/prefer-wheel/datetime.e2e.js +15 -0
  54. package/dist/collection/components/datetime/test/set-value/datetime.e2e.js +9 -12
  55. package/dist/collection/components/datetime/utils/data.js +14 -6
  56. package/dist/collection/components/datetime/utils/parse.js +18 -1
  57. package/dist/collection/components/datetime-button/datetime-button.js +3 -0
  58. package/dist/collection/components/radio/radio.ios.css +67 -0
  59. package/dist/collection/components/radio/radio.js +24 -4
  60. package/dist/collection/components/radio/radio.md.css +67 -0
  61. package/dist/collection/components/radio/test/item/radio.e2e.js +15 -0
  62. package/dist/collection/components/radio/test/label-placement/radio.e2e.js +31 -0
  63. package/dist/collection/components/range/range.ios.css +66 -1
  64. package/dist/collection/components/range/range.js +3 -3
  65. package/dist/collection/components/range/range.md.css +66 -1
  66. package/dist/collection/components/range/test/item/range.e2e.js +15 -0
  67. package/dist/collection/components/range/test/label/range.e2e.js +59 -0
  68. package/dist/collection/components/toggle/test/item/toggle.e2e.js +15 -0
  69. package/dist/collection/components/toggle/test/label/toggle.e2e.js +27 -0
  70. package/dist/collection/components/toggle/toggle.ios.css +67 -1
  71. package/dist/collection/components/toggle/toggle.js +24 -4
  72. package/dist/collection/components/toggle/toggle.md.css +67 -1
  73. package/dist/collection/index.js +1 -0
  74. package/dist/collection/utils/forms/notch-controller.js +1 -1
  75. package/dist/docs.json +147 -18
  76. package/dist/esm/{data-4b448e3a.js → data-009dbf15.js} +31 -7
  77. package/dist/esm/index.js +2 -2
  78. package/dist/esm/ion-checkbox.entry.js +5 -3
  79. package/dist/esm/ion-datetime-button.entry.js +4 -1
  80. package/dist/esm/ion-datetime_3.entry.js +132 -93
  81. package/dist/esm/ion-input.entry.js +1 -1
  82. package/dist/esm/ion-radio_2.entry.js +5 -3
  83. package/dist/esm/ion-range.entry.js +2 -2
  84. package/dist/esm/ion-select_3.entry.js +1 -1
  85. package/dist/esm/ion-textarea.entry.js +1 -1
  86. package/dist/esm/ion-toggle.entry.js +5 -3
  87. package/dist/esm/ionic.js +1 -1
  88. package/dist/esm/loader.js +1 -1
  89. package/dist/esm/{notch-controller-cb5bbe6a.js → notch-controller-8c9c0e54.js} +1 -1
  90. package/dist/esm-es5/data-009dbf15.js +4 -0
  91. package/dist/esm-es5/index.js +1 -1
  92. package/dist/esm-es5/ion-checkbox.entry.js +1 -1
  93. package/dist/esm-es5/ion-datetime-button.entry.js +1 -1
  94. package/dist/esm-es5/ion-datetime_3.entry.js +1 -1
  95. package/dist/esm-es5/ion-input.entry.js +1 -1
  96. package/dist/esm-es5/ion-radio_2.entry.js +1 -1
  97. package/dist/esm-es5/ion-range.entry.js +1 -1
  98. package/dist/esm-es5/ion-select_3.entry.js +1 -1
  99. package/dist/esm-es5/ion-textarea.entry.js +1 -1
  100. package/dist/esm-es5/ion-toggle.entry.js +1 -1
  101. package/dist/esm-es5/ionic.js +1 -1
  102. package/dist/esm-es5/loader.js +1 -1
  103. package/dist/html.html-data.json +52 -4
  104. package/dist/ionic/index.esm.js +1 -1
  105. package/dist/ionic/ionic.esm.js +1 -1
  106. package/dist/ionic/{p-dc4c9086.system.entry.js → p-264b5dd9.system.entry.js} +1 -1
  107. package/dist/ionic/{p-4d2807fa.entry.js → p-37c9e295.entry.js} +1 -1
  108. package/dist/ionic/p-3d575698.system.entry.js +4 -0
  109. package/dist/ionic/p-44ef7224.system.js +4 -0
  110. package/dist/ionic/{p-1c236453.system.entry.js → p-46058a1e.system.entry.js} +1 -1
  111. package/dist/ionic/{p-68deb933.entry.js → p-52edd6b0.entry.js} +1 -1
  112. package/dist/ionic/p-60f22efc.system.entry.js +4 -0
  113. package/dist/ionic/p-6d640b2b.entry.js +4 -0
  114. package/dist/ionic/p-74ef17ea.system.entry.js +4 -0
  115. package/dist/ionic/p-7cfd253c.js +4 -0
  116. package/dist/ionic/p-9fccec52.system.js +4 -0
  117. package/dist/ionic/p-aa702daf.entry.js +4 -0
  118. package/dist/ionic/p-ab051966.entry.js +4 -0
  119. package/dist/ionic/p-c005eb67.system.entry.js +4 -0
  120. package/dist/ionic/p-c68a8814.system.js +1 -1
  121. package/dist/ionic/p-cc758251.system.entry.js +4 -0
  122. package/dist/ionic/{p-0ce43a8f.system.entry.js → p-d0d09178.system.entry.js} +1 -1
  123. package/dist/ionic/p-dfee12f6.entry.js +4 -0
  124. package/dist/ionic/{p-8db97b90.entry.js → p-e3ca182a.entry.js} +1 -1
  125. package/dist/ionic/p-f0c28fbd.entry.js +4 -0
  126. package/dist/ionic/p-fdfc7b98.entry.js +4 -0
  127. package/dist/ionic/p-ff0611e6.system.entry.js +4 -0
  128. package/dist/types/components/checkbox/checkbox.d.ts +8 -1
  129. package/dist/types/components/datetime/datetime.d.ts +23 -10
  130. package/dist/types/components/datetime/utils/data.d.ts +1 -1
  131. package/dist/types/components/datetime/utils/parse.d.ts +3 -3
  132. package/dist/types/components/radio/radio.d.ts +8 -1
  133. package/dist/types/components/range/range.d.ts +2 -1
  134. package/dist/types/components/toast/toast-interface.d.ts +3 -0
  135. package/dist/types/components/toggle/toggle.d.ts +8 -1
  136. package/dist/types/components.d.ts +40 -16
  137. package/dist/types/index.d.ts +1 -0
  138. package/dist/types/interface.d.ts +1 -0
  139. package/hydrate/index.js +193 -111
  140. package/package.json +1 -1
  141. package/dist/esm-es5/data-4b448e3a.js +0 -4
  142. package/dist/ionic/p-17e961cf.entry.js +0 -4
  143. package/dist/ionic/p-1b533a76.system.entry.js +0 -4
  144. package/dist/ionic/p-44a6f15d.system.entry.js +0 -4
  145. package/dist/ionic/p-4f7109fa.entry.js +0 -4
  146. package/dist/ionic/p-601fd53c.js +0 -4
  147. package/dist/ionic/p-7106c152.system.js +0 -4
  148. package/dist/ionic/p-9eaaa910.entry.js +0 -4
  149. package/dist/ionic/p-af6125d1.system.entry.js +0 -4
  150. package/dist/ionic/p-b8d7f0eb.system.entry.js +0 -4
  151. package/dist/ionic/p-b9179eab.entry.js +0 -4
  152. package/dist/ionic/p-cc8910e7.system.js +0 -4
  153. package/dist/ionic/p-dc7d392d.entry.js +0 -4
  154. package/dist/ionic/p-e65aed58.system.entry.js +0 -4
  155. package/dist/ionic/p-f3ec1f2b.entry.js +0 -4
  156. package/dist/ionic/p-fb26fa56.system.entry.js +0 -4
  157. /package/dist/esm-es5/{notch-controller-cb5bbe6a.js → notch-controller-8c9c0e54.js} +0 -0
  158. /package/dist/ionic/{p-4b37d92d.js → p-3bad5c1a.js} +0 -0
  159. /package/dist/ionic/{p-46b2991d.system.js → p-ab4cff27.system.js} +0 -0
@@ -220,12 +220,20 @@ export const generateTime = (refParts, hourCycle = 'h12', minParts, maxParts, ho
220
220
  * Given DatetimeParts, generate the previous,
221
221
  * current, and and next months.
222
222
  */
223
- export const generateMonths = (refParts) => {
224
- return [
225
- getPreviousMonth(refParts),
226
- { month: refParts.month, year: refParts.year, day: refParts.day },
227
- getNextMonth(refParts),
228
- ];
223
+ export const generateMonths = (refParts, forcedDate) => {
224
+ const current = { month: refParts.month, year: refParts.year, day: refParts.day };
225
+ /**
226
+ * If we're forcing a month to appear, and it's different from the current month,
227
+ * ensure it appears by replacing the next or previous month as appropriate.
228
+ */
229
+ if (forcedDate !== undefined && (refParts.month !== forcedDate.month || refParts.year !== forcedDate.year)) {
230
+ const forced = { month: forcedDate.month, year: forcedDate.year, day: forcedDate.day };
231
+ const forcedMonthIsBefore = isBefore(forced, current);
232
+ return forcedMonthIsBefore
233
+ ? [forced, current, getNextMonth(refParts)]
234
+ : [getPreviousMonth(refParts), current, forced];
235
+ }
236
+ return [getPreviousMonth(refParts), current, getNextMonth(refParts)];
229
237
  };
230
238
  export const getMonthColumnData = (locale, refParts, minParts, maxParts, monthValues, formatOptions = {
231
239
  month: 'long',
@@ -1,6 +1,7 @@
1
1
  /*!
2
2
  * (C) Ionic http://ionicframework.com - MIT License
3
3
  */
4
+ import { printIonWarning } from "../../../utils/logging/index";
4
5
  import { isAfter, isBefore } from "./comparison";
5
6
  import { getNumDaysInMonth } from "./helpers";
6
7
  const ISO_8601_REGEXP =
@@ -47,7 +48,22 @@ export const getPartsFromCalendarDay = (el) => {
47
48
  };
48
49
  export function parseDate(val) {
49
50
  if (Array.isArray(val)) {
50
- return val.map((valStr) => parseDate(valStr));
51
+ const parsedArray = [];
52
+ for (const valStr of val) {
53
+ const parsedVal = parseDate(valStr);
54
+ /**
55
+ * If any of the values weren't parsed correctly, consider
56
+ * the entire batch incorrect. This simplifies the type
57
+ * signatures by having "undefined" be a general error case
58
+ * instead of returning (Datetime | undefined)[], which is
59
+ * harder for TS to perform type narrowing on.
60
+ */
61
+ if (!parsedVal) {
62
+ return undefined;
63
+ }
64
+ parsedArray.push(parsedVal);
65
+ }
66
+ return parsedArray;
51
67
  }
52
68
  // manually parse IS0 cuz Date.parse cannot be trusted
53
69
  // ISO 8601 format: 1994-12-15T13:47:20Z
@@ -67,6 +83,7 @@ export function parseDate(val) {
67
83
  }
68
84
  if (parse === null) {
69
85
  // wasn't able to parse the ISO datetime
86
+ printIonWarning(`Unable to parse date string: ${val}. Please provide a valid ISO 8601 datetime string.`);
70
87
  return undefined;
71
88
  }
72
89
  // ensure all the parse values exist with at least 0
@@ -54,6 +54,9 @@ export class DatetimeButton {
54
54
  * to today's date and time if no value is set.
55
55
  */
56
56
  const parsedDatetimes = parseDate(parsedValues.length > 0 ? parsedValues : [getToday()]);
57
+ if (!parsedDatetimes) {
58
+ return;
59
+ }
57
60
  /**
58
61
  * If developers incorrectly use multiple="true"
59
62
  * with non "date" datetimes, then just select
@@ -147,6 +147,15 @@ input {
147
147
  margin-bottom: 10px;
148
148
  }
149
149
 
150
+ :host(.in-item.radio-label-placement-stacked) .label-text-wrapper {
151
+ margin-top: 10px;
152
+ margin-bottom: 16px;
153
+ }
154
+
155
+ :host(.in-item.radio-label-placement-stacked) .native-wrapper {
156
+ margin-bottom: 10px;
157
+ }
158
+
150
159
  /**
151
160
  * If no label text is placed into the slot
152
161
  * then the element should be hidden otherwise
@@ -173,6 +182,14 @@ input {
173
182
  justify-content: end;
174
183
  }
175
184
 
185
+ :host(.radio-alignment-start) .radio-wrapper {
186
+ align-items: start;
187
+ }
188
+
189
+ :host(.radio-alignment-center) .radio-wrapper {
190
+ align-items: center;
191
+ }
192
+
176
193
  /**
177
194
  * Label is on the left of the radio in LTR and
178
195
  * on the right in RTL.
@@ -235,6 +252,56 @@ input {
235
252
  min-width: 100px;
236
253
  }
237
254
 
255
+ /**
256
+ * Label is on top of the radio.
257
+ */
258
+ :host(.radio-label-placement-stacked) .radio-wrapper {
259
+ flex-direction: column;
260
+ }
261
+
262
+ :host(.radio-label-placement-stacked) .label-text-wrapper {
263
+ transform: scale(0.75);
264
+ /**
265
+ * The margin between the label and
266
+ * the radio should be on the bottom
267
+ * when the label sits on top.
268
+ */
269
+ margin-left: 0;
270
+ margin-right: 0;
271
+ margin-bottom: 16px;
272
+ /**
273
+ * Label text should not extend
274
+ * beyond the bounds of the radio.
275
+ */
276
+ max-width: calc(100% / 0.75);
277
+ }
278
+
279
+ :host(.radio-label-placement-stacked.radio-alignment-start) .label-text-wrapper {
280
+ transform-origin: left top;
281
+ }
282
+ :host-context([dir=rtl]):host(.radio-label-placement-stacked.radio-alignment-start) .label-text-wrapper, :host-context([dir=rtl]).radio-label-placement-stacked.radio-alignment-start .label-text-wrapper {
283
+ transform-origin: right top;
284
+ }
285
+
286
+ @supports selector(:dir(rtl)) {
287
+ :host(.radio-label-placement-stacked.radio-alignment-start) .label-text-wrapper:dir(rtl) {
288
+ transform-origin: right top;
289
+ }
290
+ }
291
+
292
+ :host(.radio-label-placement-stacked.radio-alignment-center) .label-text-wrapper {
293
+ transform-origin: center top;
294
+ }
295
+ :host-context([dir=rtl]):host(.radio-label-placement-stacked.radio-alignment-center) .label-text-wrapper, :host-context([dir=rtl]).radio-label-placement-stacked.radio-alignment-center .label-text-wrapper {
296
+ transform-origin: calc(100% - center) top;
297
+ }
298
+
299
+ @supports selector(:dir(rtl)) {
300
+ :host(.radio-label-placement-stacked.radio-alignment-center) .label-text-wrapper:dir(rtl) {
301
+ transform-origin: calc(100% - center) top;
302
+ }
303
+ }
304
+
238
305
  :host {
239
306
  --color-checked: var(--ion-color-primary, #3880ff);
240
307
  }
@@ -70,6 +70,7 @@ export class Radio {
70
70
  this.labelPlacement = 'start';
71
71
  this.legacy = undefined;
72
72
  this.justify = 'space-between';
73
+ this.alignment = 'center';
73
74
  }
74
75
  valueChanged() {
75
76
  /**
@@ -134,7 +135,7 @@ export class Radio {
134
135
  return legacyFormController.hasLegacyControl() ? this.renderLegacyRadio() : this.renderRadio();
135
136
  }
136
137
  renderRadio() {
137
- const { checked, disabled, color, el, justify, labelPlacement, hasLabel, buttonTabindex } = this;
138
+ const { checked, disabled, color, el, justify, labelPlacement, hasLabel, buttonTabindex, alignment } = this;
138
139
  const mode = getIonMode(this);
139
140
  const inItem = hostContext('ion-item', el);
140
141
  return (h(Host, { onFocus: this.onFocus, onBlur: this.onBlur, onClick: this.onClick, class: createColorClasses(color, {
@@ -143,6 +144,7 @@ export class Radio {
143
144
  'radio-checked': checked,
144
145
  'radio-disabled': disabled,
145
146
  [`radio-justify-${justify}`]: true,
147
+ [`radio-alignment-${alignment}`]: true,
146
148
  [`radio-label-placement-${labelPlacement}`]: true,
147
149
  // Focus and active styling should not apply when the radio is in an item
148
150
  'ion-activatable': !inItem,
@@ -275,15 +277,15 @@ Developers can dismiss this warning by removing their usage of the "legacy" prop
275
277
  "type": "string",
276
278
  "mutable": false,
277
279
  "complexType": {
278
- "original": "'start' | 'end' | 'fixed'",
279
- "resolved": "\"end\" | \"fixed\" | \"start\"",
280
+ "original": "'start' | 'end' | 'fixed' | 'stacked'",
281
+ "resolved": "\"end\" | \"fixed\" | \"stacked\" | \"start\"",
280
282
  "references": {}
281
283
  },
282
284
  "required": false,
283
285
  "optional": false,
284
286
  "docs": {
285
287
  "tags": [],
286
- "text": "Where to place the label relative to the radio.\n`\"start\"`: The label will appear to the left of the radio in LTR and to the right in RTL.\n`\"end\"`: The label will appear to the right of the radio in LTR and to the left in RTL.\n`\"fixed\"`: The label has the same behavior as `\"start\"` except it also has a fixed width. Long text will be truncated with ellipses (\"...\")."
288
+ "text": "Where to place the label relative to the radio.\n`\"start\"`: The label will appear to the left of the radio in LTR and to the right in RTL.\n`\"end\"`: The label will appear to the right of the radio in LTR and to the left in RTL.\n`\"fixed\"`: The label has the same behavior as `\"start\"` except it also has a fixed width. Long text will be truncated with ellipses (\"...\").\n`\"stacked\"`: The label will appear above the radio regardless of the direction. The alignment of the label can be controlled with the `alignment` property."
287
289
  },
288
290
  "attribute": "label-placement",
289
291
  "reflect": false,
@@ -323,6 +325,24 @@ Developers can dismiss this warning by removing their usage of the "legacy" prop
323
325
  "attribute": "justify",
324
326
  "reflect": false,
325
327
  "defaultValue": "'space-between'"
328
+ },
329
+ "alignment": {
330
+ "type": "string",
331
+ "mutable": false,
332
+ "complexType": {
333
+ "original": "'start' | 'center'",
334
+ "resolved": "\"center\" | \"start\"",
335
+ "references": {}
336
+ },
337
+ "required": false,
338
+ "optional": false,
339
+ "docs": {
340
+ "tags": [],
341
+ "text": "How to control the alignment of the radio and label on the cross axis.\n`\"start\"`: The label and control will appear on the left of the cross axis in LTR, and on the right side in RTL.\n`\"center\"`: The label and control will appear at the center of the cross axis in both LTR and RTL."
342
+ },
343
+ "attribute": "alignment",
344
+ "reflect": false,
345
+ "defaultValue": "'center'"
326
346
  }
327
347
  };
328
348
  }
@@ -147,6 +147,15 @@ input {
147
147
  margin-bottom: 10px;
148
148
  }
149
149
 
150
+ :host(.in-item.radio-label-placement-stacked) .label-text-wrapper {
151
+ margin-top: 10px;
152
+ margin-bottom: 16px;
153
+ }
154
+
155
+ :host(.in-item.radio-label-placement-stacked) .native-wrapper {
156
+ margin-bottom: 10px;
157
+ }
158
+
150
159
  /**
151
160
  * If no label text is placed into the slot
152
161
  * then the element should be hidden otherwise
@@ -173,6 +182,14 @@ input {
173
182
  justify-content: end;
174
183
  }
175
184
 
185
+ :host(.radio-alignment-start) .radio-wrapper {
186
+ align-items: start;
187
+ }
188
+
189
+ :host(.radio-alignment-center) .radio-wrapper {
190
+ align-items: center;
191
+ }
192
+
176
193
  /**
177
194
  * Label is on the left of the radio in LTR and
178
195
  * on the right in RTL.
@@ -235,6 +252,56 @@ input {
235
252
  min-width: 100px;
236
253
  }
237
254
 
255
+ /**
256
+ * Label is on top of the radio.
257
+ */
258
+ :host(.radio-label-placement-stacked) .radio-wrapper {
259
+ flex-direction: column;
260
+ }
261
+
262
+ :host(.radio-label-placement-stacked) .label-text-wrapper {
263
+ transform: scale(0.75);
264
+ /**
265
+ * The margin between the label and
266
+ * the radio should be on the bottom
267
+ * when the label sits on top.
268
+ */
269
+ margin-left: 0;
270
+ margin-right: 0;
271
+ margin-bottom: 16px;
272
+ /**
273
+ * Label text should not extend
274
+ * beyond the bounds of the radio.
275
+ */
276
+ max-width: calc(100% / 0.75);
277
+ }
278
+
279
+ :host(.radio-label-placement-stacked.radio-alignment-start) .label-text-wrapper {
280
+ transform-origin: left top;
281
+ }
282
+ :host-context([dir=rtl]):host(.radio-label-placement-stacked.radio-alignment-start) .label-text-wrapper, :host-context([dir=rtl]).radio-label-placement-stacked.radio-alignment-start .label-text-wrapper {
283
+ transform-origin: right top;
284
+ }
285
+
286
+ @supports selector(:dir(rtl)) {
287
+ :host(.radio-label-placement-stacked.radio-alignment-start) .label-text-wrapper:dir(rtl) {
288
+ transform-origin: right top;
289
+ }
290
+ }
291
+
292
+ :host(.radio-label-placement-stacked.radio-alignment-center) .label-text-wrapper {
293
+ transform-origin: center top;
294
+ }
295
+ :host-context([dir=rtl]):host(.radio-label-placement-stacked.radio-alignment-center) .label-text-wrapper, :host-context([dir=rtl]).radio-label-placement-stacked.radio-alignment-center .label-text-wrapper {
296
+ transform-origin: calc(100% - center) top;
297
+ }
298
+
299
+ @supports selector(:dir(rtl)) {
300
+ :host(.radio-label-placement-stacked.radio-alignment-center) .label-text-wrapper:dir(rtl) {
301
+ transform-origin: calc(100% - center) top;
302
+ }
303
+ }
304
+
238
305
  :host {
239
306
  --color: rgb(var(--ion-text-color-rgb, 0, 0, 0), 0.6);
240
307
  --color-checked: var(--ion-color-primary, #3880ff);
@@ -51,4 +51,19 @@ configs({ directions: ['ltr'], modes: ['md'] }).forEach(({ title, screenshot, co
51
51
  await expect(list).toHaveScreenshot(screenshot(`radio-long-label-in-item`));
52
52
  });
53
53
  });
54
+ test.describe(title('radio: stacked label in item'), () => {
55
+ test('should render margins correctly when using stacked label in item', async ({ page }) => {
56
+ await page.setContent(`
57
+ <ion-list>
58
+ <ion-radio-group>
59
+ <ion-item>
60
+ <ion-radio label-placement="stacked">Enable Notifications</ion-radio>
61
+ </ion-item>
62
+ </ion-radio-group>
63
+ </ion-list>
64
+ `, config);
65
+ const list = page.locator('ion-list');
66
+ await expect(list).toHaveScreenshot(screenshot(`radio-stacked-label-in-item`));
67
+ });
68
+ });
54
69
  });
@@ -100,5 +100,36 @@ configs().forEach(({ title, screenshot, config }) => {
100
100
  await expect(radio).toHaveScreenshot(screenshot(`radio-label-fixed-justify-space-between`));
101
101
  });
102
102
  });
103
+ test.describe('radio: stacked placement', () => {
104
+ test('should align the label to the start of the container in the stacked position', async ({ page }) => {
105
+ await page.setContent(`
106
+ <ion-radio-group value="1">
107
+ <ion-radio label-placement="stacked" alignment="start" style="width: 200px" value="1">This is a long label</ion-radio>
108
+ </ion-radio-group>
109
+ `, config);
110
+ const radio = page.locator('ion-radio');
111
+ await expect(radio).toHaveScreenshot(screenshot(`radio-label-stacked-align-start`));
112
+ });
113
+ test('should align the label to the center of the container in the stacked position', async ({ page }) => {
114
+ await page.setContent(`
115
+ <ion-radio-group value="1">
116
+ <ion-radio label-placement="stacked" alignment="center" style="width: 200px" value="1">This is a long label</ion-radio>
117
+ </ion-radio-group>
118
+ `, config);
119
+ const radio = page.locator('ion-radio');
120
+ await expect(radio).toHaveScreenshot(screenshot(`radio-label-stacked-align-center`));
121
+ });
122
+ });
123
+ });
124
+ });
125
+ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config, screenshot }) => {
126
+ test.describe(title('radio: stacked long label'), () => {
127
+ test('long label should truncate', async ({ page }) => {
128
+ await page.setContent(`
129
+ <ion-radio label-placement="stacked" alignment="start" style="width: 200px">Enable Notifications Enable Notifications Enable Notifications Enable Notifications Enable Notifications Enable Notifications Enable Notifications</ion-radio>
130
+ `, config);
131
+ const radio = page.locator('ion-radio');
132
+ await expect(radio).toHaveScreenshot(screenshot(`radio-label-stacked-long-label`));
133
+ });
103
134
  });
104
135
  });
@@ -363,6 +363,50 @@
363
363
  max-width: 200px;
364
364
  }
365
365
 
366
+ /**
367
+ * Label is on top of the range.
368
+ */
369
+ :host(.range-label-placement-stacked) .range-wrapper {
370
+ flex-direction: column;
371
+ align-items: stretch;
372
+ }
373
+
374
+ :host(.range-label-placement-stacked) .label-text-wrapper {
375
+ transform-origin: left top;
376
+ transform: scale(0.75);
377
+ /**
378
+ * The margin between the label and
379
+ * the range should be on the bottom
380
+ * when the label sits on top.
381
+ */
382
+ margin-left: 0;
383
+ margin-right: 0;
384
+ margin-bottom: 16px;
385
+ /**
386
+ * Label text should not extend
387
+ * beyond the bounds of the range.
388
+ */
389
+ max-width: calc(100% / 0.75);
390
+ }
391
+ :host-context([dir=rtl]):host(.range-label-placement-stacked) .label-text-wrapper, :host-context([dir=rtl]).range-label-placement-stacked .label-text-wrapper {
392
+ transform-origin: right top;
393
+ }
394
+
395
+ @supports selector(:dir(rtl)) {
396
+ :host(.range-label-placement-stacked) .label-text-wrapper:dir(rtl) {
397
+ transform-origin: right top;
398
+ }
399
+ }
400
+
401
+ :host(.in-item.range-label-placement-stacked) .label-text-wrapper {
402
+ margin-top: 10px;
403
+ margin-bottom: 16px;
404
+ }
405
+
406
+ :host(.in-item.range-label-placement-stacked) .native-wrapper {
407
+ margin-bottom: 0px;
408
+ }
409
+
366
410
  :host {
367
411
  --knob-border-radius: 50%;
368
412
  --knob-background: #ffffff;
@@ -407,10 +451,31 @@
407
451
  margin-bottom: 0;
408
452
  }
409
453
 
410
- :host(.range-has-pin) {
454
+ :host(.range-has-pin:not(.range-label-placement-stacked)) {
455
+ /**
456
+ * The pin should not overlap any elements that are
457
+ * above the range. By adding padding to the top of the
458
+ * range, it provides a buffer for the pin to move into
459
+ * when it is pressed.
460
+ *
461
+ * The padding is not included when the label is stacked
462
+ * because the pin is below the label.
463
+ * It still requires a buffer to prevent the pin from
464
+ * overlapping the range. The buffer is added to the
465
+ * bottom of the range label instead of the host.
466
+ */
411
467
  padding-top: 20px;
412
468
  }
413
469
 
470
+ :host(.range-has-pin.range-label-placement-stacked) .label-text-wrapper {
471
+ /**
472
+ * The pin should not overlap the stacked label. By adding
473
+ * margin to the bottom of the label, it provides a buffer
474
+ * for the pin to move into when it is pressed.
475
+ */
476
+ margin-bottom: 20px;
477
+ }
478
+
414
479
  .range-bar-active {
415
480
  bottom: 0;
416
481
  width: auto;
@@ -808,15 +808,15 @@ Developers can dismiss this warning by removing their usage of the "legacy" prop
808
808
  "type": "string",
809
809
  "mutable": false,
810
810
  "complexType": {
811
- "original": "'start' | 'end' | 'fixed'",
812
- "resolved": "\"end\" | \"fixed\" | \"start\"",
811
+ "original": "'start' | 'end' | 'fixed' | 'stacked'",
812
+ "resolved": "\"end\" | \"fixed\" | \"stacked\" | \"start\"",
813
813
  "references": {}
814
814
  },
815
815
  "required": false,
816
816
  "optional": false,
817
817
  "docs": {
818
818
  "tags": [],
819
- "text": "Where to place the label relative to the range.\n`\"start\"`: The label will appear to the left of the range in LTR and to the right in RTL.\n`\"end\"`: The label will appear to the right of the range in LTR and to the left in RTL.\n`\"fixed\"`: The label has the same behavior as `\"start\"` except it also has a fixed width. Long text will be truncated with ellipses (\"...\")."
819
+ "text": "Where to place the label relative to the range.\n`\"start\"`: The label will appear to the left of the range in LTR and to the right in RTL.\n`\"end\"`: The label will appear to the right of the range in LTR and to the left in RTL.\n`\"fixed\"`: The label has the same behavior as `\"start\"` except it also has a fixed width. Long text will be truncated with ellipses (\"...\").\n`\"stacked\"`: The label will appear above the range regardless of the direction."
820
820
  },
821
821
  "attribute": "label-placement",
822
822
  "reflect": false,
@@ -363,6 +363,50 @@
363
363
  max-width: 200px;
364
364
  }
365
365
 
366
+ /**
367
+ * Label is on top of the range.
368
+ */
369
+ :host(.range-label-placement-stacked) .range-wrapper {
370
+ flex-direction: column;
371
+ align-items: stretch;
372
+ }
373
+
374
+ :host(.range-label-placement-stacked) .label-text-wrapper {
375
+ transform-origin: left top;
376
+ transform: scale(0.75);
377
+ /**
378
+ * The margin between the label and
379
+ * the range should be on the bottom
380
+ * when the label sits on top.
381
+ */
382
+ margin-left: 0;
383
+ margin-right: 0;
384
+ margin-bottom: 16px;
385
+ /**
386
+ * Label text should not extend
387
+ * beyond the bounds of the range.
388
+ */
389
+ max-width: calc(100% / 0.75);
390
+ }
391
+ :host-context([dir=rtl]):host(.range-label-placement-stacked) .label-text-wrapper, :host-context([dir=rtl]).range-label-placement-stacked .label-text-wrapper {
392
+ transform-origin: right top;
393
+ }
394
+
395
+ @supports selector(:dir(rtl)) {
396
+ :host(.range-label-placement-stacked) .label-text-wrapper:dir(rtl) {
397
+ transform-origin: right top;
398
+ }
399
+ }
400
+
401
+ :host(.in-item.range-label-placement-stacked) .label-text-wrapper {
402
+ margin-top: 10px;
403
+ margin-bottom: 16px;
404
+ }
405
+
406
+ :host(.in-item.range-label-placement-stacked) .native-wrapper {
407
+ margin-bottom: 0px;
408
+ }
409
+
366
410
  :host {
367
411
  --knob-border-radius: 50%;
368
412
  --knob-background: var(--bar-background-active);
@@ -423,10 +467,31 @@
423
467
  margin-bottom: 0;
424
468
  }
425
469
 
426
- :host(.range-has-pin) {
470
+ :host(.range-has-pin:not(.range-label-placement-stacked)) {
471
+ /**
472
+ * The pin should not overlap any elements that are
473
+ * above the range. By adding padding to the top of the
474
+ * range, it provides a buffer for the pin to move into
475
+ * when it is pressed.
476
+ *
477
+ * The padding is not included when the label is stacked
478
+ * because the pin is below the label.
479
+ * It still requires a buffer to prevent the pin from
480
+ * overlapping the range. The buffer is added to the
481
+ * bottom of the range label instead of the host.
482
+ */
427
483
  padding-top: 28px;
428
484
  }
429
485
 
486
+ :host(.range-has-pin.range-label-placement-stacked) .label-text-wrapper {
487
+ /**
488
+ * The pin should not overlap the stacked label. By adding
489
+ * margin to the bottom of the label, it provides a buffer
490
+ * for the pin to move into when it is pressed.
491
+ */
492
+ margin-bottom: 28px;
493
+ }
494
+
430
495
  .range-bar-active {
431
496
  bottom: 0;
432
497
  width: auto;
@@ -42,3 +42,18 @@ configs({ directions: ['ltr'] }).forEach(({ title, screenshot, config }) => {
42
42
  });
43
43
  });
44
44
  });
45
+ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config, screenshot }) => {
46
+ test.describe(title('range: stacked label in item'), () => {
47
+ test('should render margins correctly when using stacked label in item', async ({ page }) => {
48
+ await page.setContent(`
49
+ <ion-list>
50
+ <ion-item>
51
+ <ion-range label="Temperature" label-placement="stacked"></ion-range>
52
+ </ion-item>
53
+ </ion-list>
54
+ `, config);
55
+ const list = page.locator('ion-list');
56
+ await expect(list).toHaveScreenshot(screenshot(`range-stacked-label-in-item`));
57
+ });
58
+ });
59
+ });
@@ -40,6 +40,17 @@ configs().forEach(({ title, screenshot, config }) => {
40
40
  const range = page.locator('ion-range');
41
41
  await expect(range).toHaveScreenshot(screenshot(`range-no-items-fixed`));
42
42
  });
43
+ test('should render label above the range slider', async ({ page }) => {
44
+ await page.setContent(`
45
+ <div id="container" style="padding-inline-start: 20px;">
46
+ <ion-range label-placement="stacked">
47
+ <span slot="label">Volume</span>
48
+ </ion-range>
49
+ </div>
50
+ `, config);
51
+ const range = page.locator('#container');
52
+ await expect(range).toHaveScreenshot(screenshot(`range-no-items-stacked`));
53
+ });
43
54
  });
44
55
  test.describe('range: start and end items', () => {
45
56
  test('should render a range with no visible label', async ({ page }) => {
@@ -85,6 +96,17 @@ configs().forEach(({ title, screenshot, config }) => {
85
96
  const range = page.locator('ion-range');
86
97
  await expect(range).toHaveScreenshot(screenshot(`range-items-fixed`));
87
98
  });
99
+ test('should render label above the range slider', async ({ page }) => {
100
+ await page.setContent(`
101
+ <ion-range label-placement="stacked">
102
+ <ion-icon name="volume-off" slot="start"></ion-icon>
103
+ <ion-icon name="volume-high" slot="end"></ion-icon>
104
+ <span slot="label">Volume</span>
105
+ </ion-range>
106
+ `, config);
107
+ const range = page.locator('ion-range');
108
+ await expect(range).toHaveScreenshot(screenshot(`range-items-stacked`));
109
+ });
88
110
  });
89
111
  test.describe('range: label prop', () => {
90
112
  test('should render label in the start placement', async ({ page }) => {
@@ -102,6 +124,15 @@ configs().forEach(({ title, screenshot, config }) => {
102
124
  const range = page.locator('ion-range');
103
125
  await expect(range).toHaveScreenshot(screenshot(`range-label-prop-fixed`));
104
126
  });
127
+ test('should render label above the range slider', async ({ page }) => {
128
+ await page.setContent(`
129
+ <div id="container" style="padding-inline-start: 20px;">
130
+ <ion-range label-placement="stacked" label="Volume"></ion-range>
131
+ </div>
132
+ `, config);
133
+ const range = page.locator('#container');
134
+ await expect(range).toHaveScreenshot(screenshot(`range-label-prop-stacked`));
135
+ });
105
136
  });
106
137
  });
107
138
  });
@@ -117,4 +148,32 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, screenshot, co
117
148
  await expect(range).toHaveScreenshot(screenshot(`range-label-truncate`));
118
149
  });
119
150
  });
151
+ test.describe(title('range: with pin'), () => {
152
+ test('should render pin below a stacked label', async ({ page }) => {
153
+ await page.setContent(`
154
+ <div id="container" style="padding-inline-start: 20px;">
155
+ <ion-range label-placement="stacked" pin="true">
156
+ <span slot="label">Volume</span>
157
+ </ion-range>
158
+ </div>
159
+ `, config);
160
+ const container = page.locator('#container');
161
+ const range = page.locator('ion-range');
162
+ const knob = range.locator('.range-knob-handle');
163
+ // Force the pin to show
164
+ await knob.evaluate((el) => el.classList.add('ion-focused'));
165
+ await expect(container).toHaveScreenshot(screenshot(`range-stacked-pin`));
166
+ });
167
+ });
168
+ test.describe(title('range: stacked long label'), () => {
169
+ test('long label should truncate', async ({ page }) => {
170
+ await page.setContent(`
171
+ <ion-range label-placement="stacked" style="width: 200px">
172
+ <div slot="label">Temperature Temperature Temperature Temperature Temperature Temperature Temperature Temperature Temperature </div>
173
+ </ion-radio>
174
+ `, config);
175
+ const range = page.locator('ion-range');
176
+ await expect(range).toHaveScreenshot(screenshot(`range-label-stacked-long-label`));
177
+ });
178
+ });
120
179
  });