@ionic/core 7.7.4 → 7.7.5-nightly.20240307

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.
@@ -110,7 +110,7 @@ const Checkbox = /*@__PURE__*/ proxyCustomElement(class Checkbox extends HTMLEle
110
110
  const mode = getIonMode(this);
111
111
  const path = getSVGPath(mode, indeterminate);
112
112
  renderHiddenInput(true, el, name, checked ? value : '', disabled);
113
- return (h(Host, { class: createColorClasses(color, {
113
+ return (h(Host, { "aria-checked": indeterminate ? 'mixed' : `${checked}`, class: createColorClasses(color, {
114
114
  [mode]: true,
115
115
  'in-item': hostContext('ion-item', el),
116
116
  'checkbox-checked': checked,
@@ -100,8 +100,14 @@ const Range = /*@__PURE__*/ proxyCustomElement(class Range extends HTMLElement {
100
100
  el: rangeSlider,
101
101
  gestureName: 'range',
102
102
  gesturePriority: 100,
103
- threshold: 0,
104
- onStart: (ev) => this.onStart(ev),
103
+ /**
104
+ * Provide a threshold since the drag movement
105
+ * might be a user scrolling the view.
106
+ * If this is true, then the range
107
+ * should not move.
108
+ */
109
+ threshold: 10,
110
+ onStart: () => this.onStart(),
105
111
  onMove: (ev) => this.onMove(ev),
106
112
  onEnd: (ev) => this.onEnd(ev),
107
113
  });
@@ -284,33 +290,93 @@ const Range = /*@__PURE__*/ proxyCustomElement(class Range extends HTMLElement {
284
290
  this.value = this.ensureValueInBounds(this.value);
285
291
  this.ionChange.emit({ value: this.value });
286
292
  }
287
- onStart(detail) {
288
- const { contentEl } = this;
289
- if (contentEl) {
293
+ /**
294
+ * The value should be updated on touch end or
295
+ * when the component is being dragged.
296
+ * This follows the native behavior of mobile devices.
297
+ *
298
+ * For example: When the user lifts their finger from the
299
+ * screen after tapping the bar or dragging the bar or knob.
300
+ */
301
+ onStart() {
302
+ this.ionKnobMoveStart.emit({ value: this.ensureValueInBounds(this.value) });
303
+ }
304
+ /**
305
+ * The value should be updated while dragging the
306
+ * bar or knob.
307
+ *
308
+ * While the user is dragging, the view
309
+ * should not scroll. This is to prevent the user from
310
+ * feeling disoriented while dragging.
311
+ *
312
+ * The user can scroll on the view if the knob or
313
+ * bar is not being dragged.
314
+ *
315
+ * @param detail The details of the gesture event.
316
+ */
317
+ onMove(detail) {
318
+ const { contentEl, pressedKnob } = this;
319
+ const currentX = detail.currentX;
320
+ /**
321
+ * Since the user is dragging on the bar or knob, the view should not scroll.
322
+ *
323
+ * This only needs to be done once.
324
+ */
325
+ if (contentEl && this.initialContentScrollY === undefined) {
290
326
  this.initialContentScrollY = disableContentScrollY(contentEl);
291
327
  }
292
- const rect = (this.rect = this.rangeSlider.getBoundingClientRect());
293
- const currentX = detail.currentX;
294
- // figure out which knob they started closer to
295
- let ratio = clamp(0, (currentX - rect.left) / rect.width, 1);
296
- if (isRTL(this.el)) {
297
- ratio = 1 - ratio;
328
+ /**
329
+ * The `pressedKnob` can be undefined if the user just
330
+ * started dragging the knob.
331
+ *
332
+ * This is necessary to determine which knob the user is dragging,
333
+ * especially when it's a dual knob.
334
+ * Plus, it determines when to apply certain styles.
335
+ *
336
+ * This only needs to be done once since the knob won't change
337
+ * while the user is dragging.
338
+ */
339
+ if (pressedKnob === undefined) {
340
+ this.setPressedKnob(currentX);
298
341
  }
299
- this.pressedKnob = !this.dualKnobs || Math.abs(this.ratioA - ratio) < Math.abs(this.ratioB - ratio) ? 'A' : 'B';
300
- this.setFocus(this.pressedKnob);
301
- // update the active knob's position
302
342
  this.update(currentX);
303
- this.ionKnobMoveStart.emit({ value: this.ensureValueInBounds(this.value) });
304
- }
305
- onMove(detail) {
306
- this.update(detail.currentX);
307
343
  }
344
+ /**
345
+ * The value should be updated on touch end:
346
+ * - When the user lifts their finger from the screen after
347
+ * tapping the bar.
348
+ *
349
+ * @param detail The details of the gesture or mouse event.
350
+ */
308
351
  onEnd(detail) {
309
352
  const { contentEl, initialContentScrollY } = this;
310
- if (contentEl) {
353
+ const currentX = detail.currentX || detail.clientX;
354
+ /**
355
+ * The `pressedKnob` can be undefined if the user never
356
+ * dragged the knob. They just tapped on the bar.
357
+ *
358
+ * This is necessary to determine which knob the user is changing,
359
+ * especially when it's a dual knob.
360
+ * Plus, it determines when to apply certain styles.
361
+ */
362
+ if (this.pressedKnob === undefined) {
363
+ this.setPressedKnob(currentX);
364
+ }
365
+ /**
366
+ * The user is no longer dragging the bar or
367
+ * knob (if they were dragging it).
368
+ *
369
+ * The user can now scroll on the view in the next gesture event.
370
+ */
371
+ if (contentEl && initialContentScrollY !== undefined) {
311
372
  resetContentScrollY(contentEl, initialContentScrollY);
312
373
  }
313
- this.update(detail.currentX);
374
+ // update the active knob's position
375
+ this.update(currentX);
376
+ /**
377
+ * Reset the pressed knob to undefined since the user
378
+ * may start dragging a different knob in the next gesture event.
379
+ */
314
380
  this.pressedKnob = undefined;
315
381
  this.emitValueChange();
316
382
  this.ionKnobMoveEnd.emit({ value: this.ensureValueInBounds(this.value) });
@@ -337,6 +403,16 @@ const Range = /*@__PURE__*/ proxyCustomElement(class Range extends HTMLElement {
337
403
  // Update input value
338
404
  this.updateValue();
339
405
  }
406
+ setPressedKnob(currentX) {
407
+ const rect = (this.rect = this.rangeSlider.getBoundingClientRect());
408
+ // figure out which knob they started closer to
409
+ let ratio = clamp(0, (currentX - rect.left) / rect.width, 1);
410
+ if (isRTL(this.el)) {
411
+ ratio = 1 - ratio;
412
+ }
413
+ this.pressedKnob = !this.dualKnobs || Math.abs(this.ratioA - ratio) < Math.abs(this.ratioB - ratio) ? 'A' : 'B';
414
+ this.setFocus(this.pressedKnob);
415
+ }
340
416
  get valA() {
341
417
  return ratioToValue(this.ratioA, this.min, this.max, this.step);
342
418
  }
@@ -539,7 +615,36 @@ Developers can dismiss this warning by removing their usage of the "legacy" prop
539
615
  if (!this.legacyFormController.hasLegacyControl() && this.hasLabel) {
540
616
  labelledBy = 'range-label';
541
617
  }
542
- return (h("div", { class: "range-slider", ref: (rangeEl) => (this.rangeSlider = rangeEl) }, ticks.map((tick) => (h("div", { style: tickStyle(tick), role: "presentation", class: {
618
+ return (h("div", { class: "range-slider", ref: (rangeEl) => (this.rangeSlider = rangeEl),
619
+ /**
620
+ * Since the gesture has a threshold, the value
621
+ * won't change until the user has dragged past
622
+ * the threshold. This is to prevent the range
623
+ * from moving when the user is scrolling.
624
+ *
625
+ * This results in the value not being updated
626
+ * and the event emitters not being triggered
627
+ * if the user taps on the range. This is why
628
+ * we need to listen for the "pointerUp" event.
629
+ */
630
+ onPointerUp: (ev) => {
631
+ /**
632
+ * If the user drags the knob on the web
633
+ * version (does not occur on mobile),
634
+ * the "pointerUp" event will be triggered
635
+ * along with the gesture's events.
636
+ * This leads to duplicate events.
637
+ *
638
+ * By checking if the pressedKnob is undefined,
639
+ * we can determine if the "pointerUp" event was
640
+ * triggered by a tap or a drag. If it was
641
+ * dragged, the pressedKnob will be defined.
642
+ */
643
+ if (this.pressedKnob === undefined) {
644
+ this.onStart();
645
+ this.onEnd(ev);
646
+ }
647
+ } }, ticks.map((tick) => (h("div", { style: tickStyle(tick), role: "presentation", class: {
543
648
  'range-tick': true,
544
649
  'range-tick-active': tick.active,
545
650
  }, part: tick.active ? 'tick-active' : 'tick' }))), h("div", { class: "range-bar-container" }, h("div", { class: "range-bar", role: "presentation", part: "bar" }), h("div", { class: {
@@ -112,7 +112,7 @@ const Checkbox = class {
112
112
  const mode = ionicGlobal.getIonMode(this);
113
113
  const path = getSVGPath(mode, indeterminate);
114
114
  helpers.renderHiddenInput(true, el, name, checked ? value : '', disabled);
115
- return (index.h(index.Host, { class: theme.createColorClasses(color, {
115
+ return (index.h(index.Host, { "aria-checked": indeterminate ? 'mixed' : `${checked}`, class: theme.createColorClasses(color, {
116
116
  [mode]: true,
117
117
  'in-item': theme.hostContext('ion-item', el),
118
118
  'checkbox-checked': checked,
@@ -102,8 +102,14 @@ const Range = class {
102
102
  el: rangeSlider,
103
103
  gestureName: 'range',
104
104
  gesturePriority: 100,
105
- threshold: 0,
106
- onStart: (ev) => this.onStart(ev),
105
+ /**
106
+ * Provide a threshold since the drag movement
107
+ * might be a user scrolling the view.
108
+ * If this is true, then the range
109
+ * should not move.
110
+ */
111
+ threshold: 10,
112
+ onStart: () => this.onStart(),
107
113
  onMove: (ev) => this.onMove(ev),
108
114
  onEnd: (ev) => this.onEnd(ev),
109
115
  });
@@ -286,33 +292,93 @@ const Range = class {
286
292
  this.value = this.ensureValueInBounds(this.value);
287
293
  this.ionChange.emit({ value: this.value });
288
294
  }
289
- onStart(detail) {
290
- const { contentEl } = this;
291
- if (contentEl) {
295
+ /**
296
+ * The value should be updated on touch end or
297
+ * when the component is being dragged.
298
+ * This follows the native behavior of mobile devices.
299
+ *
300
+ * For example: When the user lifts their finger from the
301
+ * screen after tapping the bar or dragging the bar or knob.
302
+ */
303
+ onStart() {
304
+ this.ionKnobMoveStart.emit({ value: this.ensureValueInBounds(this.value) });
305
+ }
306
+ /**
307
+ * The value should be updated while dragging the
308
+ * bar or knob.
309
+ *
310
+ * While the user is dragging, the view
311
+ * should not scroll. This is to prevent the user from
312
+ * feeling disoriented while dragging.
313
+ *
314
+ * The user can scroll on the view if the knob or
315
+ * bar is not being dragged.
316
+ *
317
+ * @param detail The details of the gesture event.
318
+ */
319
+ onMove(detail) {
320
+ const { contentEl, pressedKnob } = this;
321
+ const currentX = detail.currentX;
322
+ /**
323
+ * Since the user is dragging on the bar or knob, the view should not scroll.
324
+ *
325
+ * This only needs to be done once.
326
+ */
327
+ if (contentEl && this.initialContentScrollY === undefined) {
292
328
  this.initialContentScrollY = index$2.disableContentScrollY(contentEl);
293
329
  }
294
- const rect = (this.rect = this.rangeSlider.getBoundingClientRect());
295
- const currentX = detail.currentX;
296
- // figure out which knob they started closer to
297
- let ratio = helpers.clamp(0, (currentX - rect.left) / rect.width, 1);
298
- if (dir.isRTL(this.el)) {
299
- ratio = 1 - ratio;
330
+ /**
331
+ * The `pressedKnob` can be undefined if the user just
332
+ * started dragging the knob.
333
+ *
334
+ * This is necessary to determine which knob the user is dragging,
335
+ * especially when it's a dual knob.
336
+ * Plus, it determines when to apply certain styles.
337
+ *
338
+ * This only needs to be done once since the knob won't change
339
+ * while the user is dragging.
340
+ */
341
+ if (pressedKnob === undefined) {
342
+ this.setPressedKnob(currentX);
300
343
  }
301
- this.pressedKnob = !this.dualKnobs || Math.abs(this.ratioA - ratio) < Math.abs(this.ratioB - ratio) ? 'A' : 'B';
302
- this.setFocus(this.pressedKnob);
303
- // update the active knob's position
304
344
  this.update(currentX);
305
- this.ionKnobMoveStart.emit({ value: this.ensureValueInBounds(this.value) });
306
- }
307
- onMove(detail) {
308
- this.update(detail.currentX);
309
345
  }
346
+ /**
347
+ * The value should be updated on touch end:
348
+ * - When the user lifts their finger from the screen after
349
+ * tapping the bar.
350
+ *
351
+ * @param detail The details of the gesture or mouse event.
352
+ */
310
353
  onEnd(detail) {
311
354
  const { contentEl, initialContentScrollY } = this;
312
- if (contentEl) {
355
+ const currentX = detail.currentX || detail.clientX;
356
+ /**
357
+ * The `pressedKnob` can be undefined if the user never
358
+ * dragged the knob. They just tapped on the bar.
359
+ *
360
+ * This is necessary to determine which knob the user is changing,
361
+ * especially when it's a dual knob.
362
+ * Plus, it determines when to apply certain styles.
363
+ */
364
+ if (this.pressedKnob === undefined) {
365
+ this.setPressedKnob(currentX);
366
+ }
367
+ /**
368
+ * The user is no longer dragging the bar or
369
+ * knob (if they were dragging it).
370
+ *
371
+ * The user can now scroll on the view in the next gesture event.
372
+ */
373
+ if (contentEl && initialContentScrollY !== undefined) {
313
374
  index$2.resetContentScrollY(contentEl, initialContentScrollY);
314
375
  }
315
- this.update(detail.currentX);
376
+ // update the active knob's position
377
+ this.update(currentX);
378
+ /**
379
+ * Reset the pressed knob to undefined since the user
380
+ * may start dragging a different knob in the next gesture event.
381
+ */
316
382
  this.pressedKnob = undefined;
317
383
  this.emitValueChange();
318
384
  this.ionKnobMoveEnd.emit({ value: this.ensureValueInBounds(this.value) });
@@ -339,6 +405,16 @@ const Range = class {
339
405
  // Update input value
340
406
  this.updateValue();
341
407
  }
408
+ setPressedKnob(currentX) {
409
+ const rect = (this.rect = this.rangeSlider.getBoundingClientRect());
410
+ // figure out which knob they started closer to
411
+ let ratio = helpers.clamp(0, (currentX - rect.left) / rect.width, 1);
412
+ if (dir.isRTL(this.el)) {
413
+ ratio = 1 - ratio;
414
+ }
415
+ this.pressedKnob = !this.dualKnobs || Math.abs(this.ratioA - ratio) < Math.abs(this.ratioB - ratio) ? 'A' : 'B';
416
+ this.setFocus(this.pressedKnob);
417
+ }
342
418
  get valA() {
343
419
  return ratioToValue(this.ratioA, this.min, this.max, this.step);
344
420
  }
@@ -541,7 +617,36 @@ Developers can dismiss this warning by removing their usage of the "legacy" prop
541
617
  if (!this.legacyFormController.hasLegacyControl() && this.hasLabel) {
542
618
  labelledBy = 'range-label';
543
619
  }
544
- return (index.h("div", { class: "range-slider", ref: (rangeEl) => (this.rangeSlider = rangeEl) }, ticks.map((tick) => (index.h("div", { style: tickStyle(tick), role: "presentation", class: {
620
+ return (index.h("div", { class: "range-slider", ref: (rangeEl) => (this.rangeSlider = rangeEl),
621
+ /**
622
+ * Since the gesture has a threshold, the value
623
+ * won't change until the user has dragged past
624
+ * the threshold. This is to prevent the range
625
+ * from moving when the user is scrolling.
626
+ *
627
+ * This results in the value not being updated
628
+ * and the event emitters not being triggered
629
+ * if the user taps on the range. This is why
630
+ * we need to listen for the "pointerUp" event.
631
+ */
632
+ onPointerUp: (ev) => {
633
+ /**
634
+ * If the user drags the knob on the web
635
+ * version (does not occur on mobile),
636
+ * the "pointerUp" event will be triggered
637
+ * along with the gesture's events.
638
+ * This leads to duplicate events.
639
+ *
640
+ * By checking if the pressedKnob is undefined,
641
+ * we can determine if the "pointerUp" event was
642
+ * triggered by a tap or a drag. If it was
643
+ * dragged, the pressedKnob will be defined.
644
+ */
645
+ if (this.pressedKnob === undefined) {
646
+ this.onStart();
647
+ this.onEnd(ev);
648
+ }
649
+ } }, ticks.map((tick) => (index.h("div", { style: tickStyle(tick), role: "presentation", class: {
545
650
  'range-tick': true,
546
651
  'range-tick-active': tick.active,
547
652
  }, part: tick.active ? 'tick-active' : 'tick' }))), index.h("div", { class: "range-bar-container" }, index.h("div", { class: "range-bar", role: "presentation", part: "bar" }), index.h("div", { class: {
@@ -105,7 +105,7 @@ export class Checkbox {
105
105
  const mode = getIonMode(this);
106
106
  const path = getSVGPath(mode, indeterminate);
107
107
  renderHiddenInput(true, el, name, checked ? value : '', disabled);
108
- return (h(Host, { class: createColorClasses(color, {
108
+ return (h(Host, { "aria-checked": indeterminate ? 'mixed' : `${checked}`, class: createColorClasses(color, {
109
109
  [mode]: true,
110
110
  'in-item': hostContext('ion-item', el),
111
111
  'checkbox-checked': checked,
@@ -32,3 +32,15 @@ describe('ion-checkbox: disabled', () => {
32
32
  expect(checkbox.checked).toBe(false);
33
33
  });
34
34
  });
35
+ describe('ion-checkbox: indeterminate', () => {
36
+ it('should have a mixed value for aria-checked', async () => {
37
+ const page = await newSpecPage({
38
+ components: [Checkbox],
39
+ html: `
40
+ <ion-checkbox indeterminate="true">Checkbox</ion-checkbox>
41
+ `,
42
+ });
43
+ const checkbox = page.body.querySelector('ion-checkbox');
44
+ expect(checkbox.getAttribute('aria-checked')).toBe('mixed');
45
+ });
46
+ });
@@ -58,8 +58,14 @@ export class Range {
58
58
  el: rangeSlider,
59
59
  gestureName: 'range',
60
60
  gesturePriority: 100,
61
- threshold: 0,
62
- onStart: (ev) => this.onStart(ev),
61
+ /**
62
+ * Provide a threshold since the drag movement
63
+ * might be a user scrolling the view.
64
+ * If this is true, then the range
65
+ * should not move.
66
+ */
67
+ threshold: 10,
68
+ onStart: () => this.onStart(),
63
69
  onMove: (ev) => this.onMove(ev),
64
70
  onEnd: (ev) => this.onEnd(ev),
65
71
  });
@@ -242,33 +248,93 @@ export class Range {
242
248
  this.value = this.ensureValueInBounds(this.value);
243
249
  this.ionChange.emit({ value: this.value });
244
250
  }
245
- onStart(detail) {
246
- const { contentEl } = this;
247
- if (contentEl) {
251
+ /**
252
+ * The value should be updated on touch end or
253
+ * when the component is being dragged.
254
+ * This follows the native behavior of mobile devices.
255
+ *
256
+ * For example: When the user lifts their finger from the
257
+ * screen after tapping the bar or dragging the bar or knob.
258
+ */
259
+ onStart() {
260
+ this.ionKnobMoveStart.emit({ value: this.ensureValueInBounds(this.value) });
261
+ }
262
+ /**
263
+ * The value should be updated while dragging the
264
+ * bar or knob.
265
+ *
266
+ * While the user is dragging, the view
267
+ * should not scroll. This is to prevent the user from
268
+ * feeling disoriented while dragging.
269
+ *
270
+ * The user can scroll on the view if the knob or
271
+ * bar is not being dragged.
272
+ *
273
+ * @param detail The details of the gesture event.
274
+ */
275
+ onMove(detail) {
276
+ const { contentEl, pressedKnob } = this;
277
+ const currentX = detail.currentX;
278
+ /**
279
+ * Since the user is dragging on the bar or knob, the view should not scroll.
280
+ *
281
+ * This only needs to be done once.
282
+ */
283
+ if (contentEl && this.initialContentScrollY === undefined) {
248
284
  this.initialContentScrollY = disableContentScrollY(contentEl);
249
285
  }
250
- const rect = (this.rect = this.rangeSlider.getBoundingClientRect());
251
- const currentX = detail.currentX;
252
- // figure out which knob they started closer to
253
- let ratio = clamp(0, (currentX - rect.left) / rect.width, 1);
254
- if (isRTL(this.el)) {
255
- ratio = 1 - ratio;
286
+ /**
287
+ * The `pressedKnob` can be undefined if the user just
288
+ * started dragging the knob.
289
+ *
290
+ * This is necessary to determine which knob the user is dragging,
291
+ * especially when it's a dual knob.
292
+ * Plus, it determines when to apply certain styles.
293
+ *
294
+ * This only needs to be done once since the knob won't change
295
+ * while the user is dragging.
296
+ */
297
+ if (pressedKnob === undefined) {
298
+ this.setPressedKnob(currentX);
256
299
  }
257
- this.pressedKnob = !this.dualKnobs || Math.abs(this.ratioA - ratio) < Math.abs(this.ratioB - ratio) ? 'A' : 'B';
258
- this.setFocus(this.pressedKnob);
259
- // update the active knob's position
260
300
  this.update(currentX);
261
- this.ionKnobMoveStart.emit({ value: this.ensureValueInBounds(this.value) });
262
- }
263
- onMove(detail) {
264
- this.update(detail.currentX);
265
301
  }
302
+ /**
303
+ * The value should be updated on touch end:
304
+ * - When the user lifts their finger from the screen after
305
+ * tapping the bar.
306
+ *
307
+ * @param detail The details of the gesture or mouse event.
308
+ */
266
309
  onEnd(detail) {
267
310
  const { contentEl, initialContentScrollY } = this;
268
- if (contentEl) {
311
+ const currentX = detail.currentX || detail.clientX;
312
+ /**
313
+ * The `pressedKnob` can be undefined if the user never
314
+ * dragged the knob. They just tapped on the bar.
315
+ *
316
+ * This is necessary to determine which knob the user is changing,
317
+ * especially when it's a dual knob.
318
+ * Plus, it determines when to apply certain styles.
319
+ */
320
+ if (this.pressedKnob === undefined) {
321
+ this.setPressedKnob(currentX);
322
+ }
323
+ /**
324
+ * The user is no longer dragging the bar or
325
+ * knob (if they were dragging it).
326
+ *
327
+ * The user can now scroll on the view in the next gesture event.
328
+ */
329
+ if (contentEl && initialContentScrollY !== undefined) {
269
330
  resetContentScrollY(contentEl, initialContentScrollY);
270
331
  }
271
- this.update(detail.currentX);
332
+ // update the active knob's position
333
+ this.update(currentX);
334
+ /**
335
+ * Reset the pressed knob to undefined since the user
336
+ * may start dragging a different knob in the next gesture event.
337
+ */
272
338
  this.pressedKnob = undefined;
273
339
  this.emitValueChange();
274
340
  this.ionKnobMoveEnd.emit({ value: this.ensureValueInBounds(this.value) });
@@ -295,6 +361,16 @@ export class Range {
295
361
  // Update input value
296
362
  this.updateValue();
297
363
  }
364
+ setPressedKnob(currentX) {
365
+ const rect = (this.rect = this.rangeSlider.getBoundingClientRect());
366
+ // figure out which knob they started closer to
367
+ let ratio = clamp(0, (currentX - rect.left) / rect.width, 1);
368
+ if (isRTL(this.el)) {
369
+ ratio = 1 - ratio;
370
+ }
371
+ this.pressedKnob = !this.dualKnobs || Math.abs(this.ratioA - ratio) < Math.abs(this.ratioB - ratio) ? 'A' : 'B';
372
+ this.setFocus(this.pressedKnob);
373
+ }
298
374
  get valA() {
299
375
  return ratioToValue(this.ratioA, this.min, this.max, this.step);
300
376
  }
@@ -497,7 +573,36 @@ Developers can dismiss this warning by removing their usage of the "legacy" prop
497
573
  if (!this.legacyFormController.hasLegacyControl() && this.hasLabel) {
498
574
  labelledBy = 'range-label';
499
575
  }
500
- return (h("div", { class: "range-slider", ref: (rangeEl) => (this.rangeSlider = rangeEl) }, ticks.map((tick) => (h("div", { style: tickStyle(tick), role: "presentation", class: {
576
+ return (h("div", { class: "range-slider", ref: (rangeEl) => (this.rangeSlider = rangeEl),
577
+ /**
578
+ * Since the gesture has a threshold, the value
579
+ * won't change until the user has dragged past
580
+ * the threshold. This is to prevent the range
581
+ * from moving when the user is scrolling.
582
+ *
583
+ * This results in the value not being updated
584
+ * and the event emitters not being triggered
585
+ * if the user taps on the range. This is why
586
+ * we need to listen for the "pointerUp" event.
587
+ */
588
+ onPointerUp: (ev) => {
589
+ /**
590
+ * If the user drags the knob on the web
591
+ * version (does not occur on mobile),
592
+ * the "pointerUp" event will be triggered
593
+ * along with the gesture's events.
594
+ * This leads to duplicate events.
595
+ *
596
+ * By checking if the pressedKnob is undefined,
597
+ * we can determine if the "pointerUp" event was
598
+ * triggered by a tap or a drag. If it was
599
+ * dragged, the pressedKnob will be defined.
600
+ */
601
+ if (this.pressedKnob === undefined) {
602
+ this.onStart();
603
+ this.onEnd(ev);
604
+ }
605
+ } }, ticks.map((tick) => (h("div", { style: tickStyle(tick), role: "presentation", class: {
501
606
  'range-tick': true,
502
607
  'range-tick-active': tick.active,
503
608
  }, part: tick.active ? 'tick-active' : 'tick' }))), h("div", { class: "range-bar-container" }, h("div", { class: "range-bar", role: "presentation", part: "bar" }), h("div", { class: {
@@ -54,6 +54,33 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
54
54
  expect(rangeStart).toHaveReceivedEventDetail({ value: 20 });
55
55
  expect(rangeEnd).toHaveReceivedEventDetail({ value: 21 });
56
56
  });
57
+ test('should emit end event on tap', async ({ page }, testInfo) => {
58
+ testInfo.annotations.push({
59
+ type: 'issue',
60
+ description: 'https://github.com/ionic-team/ionic-framework/issues/28487',
61
+ });
62
+ await page.setContent(`<ion-range aria-label="Range" value="20"></ion-range>`, config);
63
+ const range = page.locator('ion-range');
64
+ const rangeEndSpy = await page.spyOnEvent('ionKnobMoveEnd');
65
+ const rangeBoundingBox = await range.boundingBox();
66
+ /**
67
+ * Coordinates for the click event.
68
+ * These need to be near the end of the range
69
+ * (or anything that isn't the current value).
70
+ *
71
+ * The number 50 is arbitrary, but it should be
72
+ * less than the width of the range.
73
+ */
74
+ const x = rangeBoundingBox.width - 50;
75
+ // The y coordinate is the middle of the range.
76
+ const y = rangeBoundingBox.height / 2;
77
+ // Click near the end of the range.
78
+ await range.click({
79
+ position: { x, y },
80
+ });
81
+ await rangeEndSpy.next();
82
+ expect(rangeEndSpy.length).toBe(1);
83
+ });
57
84
  // TODO FW-2873
58
85
  test.skip('should not scroll when the knob is swiped', async ({ page, skip }) => {
59
86
  skip.browser('webkit', 'mouse.wheel is not available in WebKit');
package/dist/docs.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "timestamp": "2024-03-06T17:59:51",
2
+ "timestamp": "2024-03-07T06:08:38",
3
3
  "compiler": {
4
4
  "name": "@stencil/core",
5
5
  "version": "4.12.4",
@@ -108,7 +108,7 @@ const Checkbox = class {
108
108
  const mode = getIonMode(this);
109
109
  const path = getSVGPath(mode, indeterminate);
110
110
  renderHiddenInput(true, el, name, checked ? value : '', disabled);
111
- return (h(Host, { class: createColorClasses(color, {
111
+ return (h(Host, { "aria-checked": indeterminate ? 'mixed' : `${checked}`, class: createColorClasses(color, {
112
112
  [mode]: true,
113
113
  'in-item': hostContext('ion-item', el),
114
114
  'checkbox-checked': checked,