@ionic/core 9.0.2-dev.11788201761.1a20dc3a → 9.0.2-dev.11789078072.1cdaafd9

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 (40) hide show
  1. package/components/ion-input.js +1 -1
  2. package/components/ion-select.js +1 -1
  3. package/components/ion-textarea.js +1 -1
  4. package/components/p-CobW5muY.js +4 -0
  5. package/dist/cjs/ion-input.cjs.entry.js +6 -9
  6. package/dist/cjs/ion-select_3.cjs.entry.js +57 -37
  7. package/dist/cjs/ion-textarea.cjs.entry.js +5 -8
  8. package/dist/cjs/ionic.cjs.js +1 -1
  9. package/dist/cjs/loader.cjs.js +1 -1
  10. package/dist/cjs/{slot-mutation-controller-D6IjSEIh.js → slot-mutation-controller-yNStVQqX.js} +69 -0
  11. package/dist/collection/components/input/input.js +6 -9
  12. package/dist/collection/components/select/select.js +67 -38
  13. package/dist/collection/components/textarea/textarea.js +5 -8
  14. package/dist/collection/utils/forms/click-controller.js +70 -0
  15. package/dist/collection/utils/forms/index.js +1 -0
  16. package/dist/docs.json +8 -2
  17. package/dist/esm/ion-input.entry.js +6 -9
  18. package/dist/esm/ion-select_3.entry.js +57 -37
  19. package/dist/esm/ion-textarea.entry.js +5 -8
  20. package/dist/esm/ionic.js +1 -1
  21. package/dist/esm/loader.js +1 -1
  22. package/dist/esm/{slot-mutation-controller-B5NpUZ-N.js → slot-mutation-controller-BVnANfIt.js} +68 -1
  23. package/dist/ionic/ionic.esm.js +1 -1
  24. package/dist/ionic/p-39b6b6ba.entry.js +4 -0
  25. package/dist/ionic/p-4d7779c8.entry.js +4 -0
  26. package/dist/ionic/p-91407d16.entry.js +4 -0
  27. package/dist/ionic/p-DcCEP7Ss.js +4 -0
  28. package/dist/types/components/input/input.d.ts +1 -0
  29. package/dist/types/components/select/select.d.ts +11 -0
  30. package/dist/types/components/textarea/textarea.d.ts +1 -0
  31. package/dist/types/utils/forms/click-controller.d.ts +36 -0
  32. package/dist/types/utils/forms/index.d.ts +1 -0
  33. package/hydrate/index.js +133 -52
  34. package/hydrate/index.mjs +133 -52
  35. package/package.json +1 -1
  36. package/components/p-Cw6WocLJ.js +0 -4
  37. package/dist/ionic/p-7e394cc3.entry.js +0 -4
  38. package/dist/ionic/p-CkxVYPtX.js +0 -4
  39. package/dist/ionic/p-a20a2ebe.entry.js +0 -4
  40. package/dist/ionic/p-f83b3e0c.entry.js +0 -4
@@ -28,6 +28,7 @@ export declare class Textarea implements ComponentInterface {
28
28
  private notchSpacerEl;
29
29
  private startContainerController?;
30
30
  private startContainerEl;
31
+ private clickController?;
31
32
  /**
32
33
  * The value of the textarea when the textarea is focused.
33
34
  */
@@ -0,0 +1,36 @@
1
+ export interface ClickController {
2
+ handleClickCapture: (ev: Event) => void;
3
+ }
4
+ /**
5
+ * The content slotted into a form control's start or end slot that a click
6
+ * started on, or `null` when the click did not start on slotted content.
7
+ *
8
+ * The slotted element is compared against the host in case the form control
9
+ * itself is slotted into, for example, an item. Without that check a control
10
+ * carrying slot="start"/"end" would treat every click on itself as a slotted
11
+ * click.
12
+ */
13
+ export declare const getSlottedClickContent: (ev: Event, el: HTMLElement) => HTMLElement | null;
14
+ /**
15
+ * A utility for form components that wrap their content in a <label>, such as
16
+ * ion-input, ion-textarea and ion-select.
17
+ *
18
+ * Clicking slotted content also clicks that label, and the browser follows it
19
+ * with a click on the label's control. That second click would be emitted from
20
+ * the host as a duplicate, so the slotted click is remembered and the click
21
+ * that follows it is suppressed.
22
+ *
23
+ * Browsers skip the forwarding when the click lands on interactive content,
24
+ * such as a slotted button, so the slotted click is remembered for a frame
25
+ * rather than until a forwarded click that may never arrive.
26
+ *
27
+ * @internal
28
+ * @param el - The host element.
29
+ * @param getNativeInput - A callback returning the native form control the
30
+ * label points at, for components that have one. Those components emit the
31
+ * click from the host rather than the control, so the control's click is
32
+ * always stopped and re-dispatched from the host. Omit it for components whose
33
+ * label has no `for` attribute, such as ion-select, where the browser forwards
34
+ * to an internal control that already re-bubbles targeting the host.
35
+ */
36
+ export declare const createClickController: (el: HTMLElement, getNativeInput?: () => HTMLInputElement | HTMLTextAreaElement | undefined) => ClickController;
@@ -2,4 +2,5 @@ export * from './notch-controller';
2
2
  export * from './start-container-controller';
3
3
  export * from './compare-with-utils';
4
4
  export * from './item-multiple-inputs';
5
+ export * from './click-controller';
5
6
  export * from './validity';
package/hydrate/index.js CHANGED
@@ -13040,6 +13040,73 @@ const createItemMultipleInputsObserver = (el, onChange, classNames = ['item-mult
13040
13040
  }
13041
13041
  };
13042
13042
 
13043
+ /**
13044
+ * The content slotted into a form control's start or end slot that a click
13045
+ * started on, or `null` when the click did not start on slotted content.
13046
+ *
13047
+ * The slotted element is compared against the host in case the form control
13048
+ * itself is slotted into, for example, an item. Without that check a control
13049
+ * carrying slot="start"/"end" would treat every click on itself as a slotted
13050
+ * click.
13051
+ */
13052
+ const getSlottedClickContent = (ev, el) => {
13053
+ const slotted = ev.target.closest('[slot="start"], [slot="end"]');
13054
+ return slotted !== null && slotted !== el && el.contains(slotted) ? slotted : null;
13055
+ };
13056
+ /**
13057
+ * Whether a click started on content slotted into a form control's start or
13058
+ * end slot.
13059
+ */
13060
+ const isSlottedClick = (ev, el) => getSlottedClickContent(ev, el) !== null;
13061
+ /**
13062
+ * A utility for form components that wrap their content in a <label>, such as
13063
+ * ion-input, ion-textarea and ion-select.
13064
+ *
13065
+ * Clicking slotted content also clicks that label, and the browser follows it
13066
+ * with a click on the label's control. That second click would be emitted from
13067
+ * the host as a duplicate, so the slotted click is remembered and the click
13068
+ * that follows it is suppressed.
13069
+ *
13070
+ * Browsers skip the forwarding when the click lands on interactive content,
13071
+ * such as a slotted button, so the slotted click is remembered for a frame
13072
+ * rather than until a forwarded click that may never arrive.
13073
+ *
13074
+ * @internal
13075
+ * @param el - The host element.
13076
+ * @param getNativeInput - A callback returning the native form control the
13077
+ * label points at, for components that have one. Those components emit the
13078
+ * click from the host rather than the control, so the control's click is
13079
+ * always stopped and re-dispatched from the host. Omit it for components whose
13080
+ * label has no `for` attribute, such as ion-select, where the browser forwards
13081
+ * to an internal control that already re-bubbles targeting the host.
13082
+ */
13083
+ const createClickController = (el, getNativeInput) => {
13084
+ let hasSlottedClick = false;
13085
+ const handleClickCapture = (ev) => {
13086
+ if (isSlottedClick(ev, el)) {
13087
+ hasSlottedClick = true;
13088
+ raf(() => (hasSlottedClick = false));
13089
+ return;
13090
+ }
13091
+ if (getNativeInput === undefined) {
13092
+ if (hasSlottedClick) {
13093
+ ev.stopPropagation();
13094
+ hasSlottedClick = false;
13095
+ }
13096
+ return;
13097
+ }
13098
+ const nativeInput = getNativeInput();
13099
+ if (nativeInput !== undefined && ev.target === nativeInput) {
13100
+ ev.stopPropagation();
13101
+ if (!hasSlottedClick) {
13102
+ el.click();
13103
+ }
13104
+ hasSlottedClick = false;
13105
+ }
13106
+ };
13107
+ return { handleClickCapture };
13108
+ };
13109
+
13043
13110
  /**
13044
13111
  * Checks if the form element is in an invalid state based on
13045
13112
  * Ionic validation classes.
@@ -20486,11 +20553,7 @@ class Input {
20486
20553
  * Instead, the click event from the ion-input is emitted.
20487
20554
  */
20488
20555
  onClickCapture(ev) {
20489
- const nativeInput = this.nativeInput;
20490
- if (nativeInput && ev.target === nativeInput) {
20491
- ev.stopPropagation();
20492
- this.el.click();
20493
- }
20556
+ this.clickController?.handleClickCapture(ev);
20494
20557
  }
20495
20558
  componentWillLoad() {
20496
20559
  this.inheritedAttributes = {
@@ -20510,6 +20573,7 @@ class Input {
20510
20573
  return Build.isBrowser;
20511
20574
  });
20512
20575
  this.startContainerController.calculateStartContainerWidth();
20576
+ this.clickController = createClickController(el, () => this.nativeInput);
20513
20577
  // Always set initial state
20514
20578
  this.isInvalid = checkInvalidState(el);
20515
20579
  this.debounceChanged();
@@ -20760,7 +20824,7 @@ class Input {
20760
20824
  * the input has a value or is focused.
20761
20825
  */
20762
20826
  const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || hasFocus));
20763
- return (hAsync(Host, { key: '9e9495b8f962372d526343ff76b80fb9700fce19', class: createColorClasses$1(this.color, {
20827
+ return (hAsync(Host, { key: '290cf987de670276bf40b9f19d35d3769d9c0540', class: createColorClasses$1(this.color, {
20764
20828
  [mode]: true,
20765
20829
  'has-value': hasValue,
20766
20830
  'has-focus': hasFocus,
@@ -20771,14 +20835,14 @@ class Input {
20771
20835
  'in-item': inItem,
20772
20836
  'in-item-color': hostContext('ion-item.ion-color', this.el),
20773
20837
  'input-disabled': disabled,
20774
- }) }, hAsync("label", { key: '7121f34ed782caf7a7b82703b94fc9d6c78ce5ba', class: "input-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, hasOutlineFill && this.renderOutlineContainer(), hAsync("div", { key: '0c1f8526d86c8a2433c23873fa64b1edc0eafe29', class: "input-start", ref: (el) => (this.startContainerEl = el) }, hAsync("slot", { key: 'a50323fde0fb12997c2324eded132908680ea450', name: "start" })), hAsync("div", { key: '626360c4e115a5286baab49680b2a02cd39614c8', class: "input-control" }, this.renderLabel(), hAsync("div", { key: '1cefd9d8cd6ed96fab7b58237f8f8b0c5e1be92e', class: "native-wrapper", onClick: this.onLabelClick }, hAsync("input", { key: '3dee5302f3a685a848fedd7e1face5095e3ac64b', class: "native-input", ref: (input) => (this.nativeInput = input), id: inputId, disabled: disabled, autoCapitalize: this.autocapitalize, autoComplete: this.autocomplete, autoCorrect: this.autocorrect ? 'on' : 'off', autoFocus: this.autofocus, enterKeyHint: this.enterkeyhint, inputMode: this.inputmode, min: this.min, max: this.max, minLength: this.minlength, maxLength: this.maxlength, multiple: this.multiple, name: this.name, pattern: this.pattern, placeholder: this.placeholder || '', readOnly: readonly, required: this.required, spellcheck: this.spellcheck, step: this.step, type: this.type, value: value, onInput: this.onInput, onChange: this.onChange, onBlur: this.onBlur, onFocus: this.onFocus, onKeyDown: this.onKeydown, onCompositionstart: this.onCompositionStart, onCompositionend: this.onCompositionEnd, "aria-describedby": this.getHintTextID(), "aria-invalid": this.isInvalid ? 'true' : undefined, "aria-labelledby": this.getLabelledById(), ...this.inheritedAttributes }))), hAsync("div", { key: '39fff82e00e63963fe5f17794900ace2a3faa208', class: "input-end" }, this.clearInput && !readonly && !disabled && (hAsync("button", { key: '1fa86fdb5baf4a7cd531ee0fe767a42b44fb33fc', "aria-label": "reset", type: "button", class: "input-clear-icon", onPointerDown: (ev) => {
20838
+ }) }, hAsync("label", { key: '27f4bd671f6084a7e608e4f9442192c950879dc2', class: "input-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, hasOutlineFill && this.renderOutlineContainer(), hAsync("div", { key: '7827c61ffdd02952fc8cf2096f621b50fc51ec03', class: "input-start", ref: (el) => (this.startContainerEl = el) }, hAsync("slot", { key: '1d8af52d3506742d87334b388859a8bf613e1be0', name: "start" })), hAsync("div", { key: '927a412064538c00df98fabc06a32c564d05e984', class: "input-control" }, this.renderLabel(), hAsync("div", { key: '05f80c0b11a7da57445f976bd2dbb557fa63c9bc', class: "native-wrapper", onClick: this.onLabelClick }, hAsync("input", { key: 'cb6f5d81ba7c2a04cc4c9deb5d3f5bca8562b6db', class: "native-input", ref: (input) => (this.nativeInput = input), id: inputId, disabled: disabled, autoCapitalize: this.autocapitalize, autoComplete: this.autocomplete, autoCorrect: this.autocorrect ? 'on' : 'off', autoFocus: this.autofocus, enterKeyHint: this.enterkeyhint, inputMode: this.inputmode, min: this.min, max: this.max, minLength: this.minlength, maxLength: this.maxlength, multiple: this.multiple, name: this.name, pattern: this.pattern, placeholder: this.placeholder || '', readOnly: readonly, required: this.required, spellcheck: this.spellcheck, step: this.step, type: this.type, value: value, onInput: this.onInput, onChange: this.onChange, onBlur: this.onBlur, onFocus: this.onFocus, onKeyDown: this.onKeydown, onCompositionstart: this.onCompositionStart, onCompositionend: this.onCompositionEnd, "aria-describedby": this.getHintTextID(), "aria-invalid": this.isInvalid ? 'true' : undefined, "aria-labelledby": this.getLabelledById(), ...this.inheritedAttributes }))), hAsync("div", { key: '82cd1b0458dcd491fd1a46b29764f2b79a969bc6', class: "input-end" }, this.clearInput && !readonly && !disabled && (hAsync("button", { key: 'dfcf0ed81cf36fc61b8e12ed4605ac6b6eb50dad', "aria-label": "reset", type: "button", class: "input-clear-icon", onPointerDown: (ev) => {
20775
20839
  /**
20776
20840
  * This prevents mobile browsers from
20777
20841
  * blurring the input when the clear
20778
20842
  * button is activated.
20779
20843
  */
20780
20844
  ev.preventDefault();
20781
- }, onClick: this.clearTextInput }, hAsync("ion-icon", { key: 'ced39304f97e56870f7480afa025ffe232010cfb', "aria-hidden": "true", icon: clearIconData }))), hAsync("slot", { key: 'f4098769eef1cc5aa2074c369f757ccb94a98e1c', name: "end" })), shouldRenderHighlight && hAsync("div", { key: '258fe6ff8f9f57836f2a3a0b6163bd2b7dbf0699', class: "input-highlight" })), this.renderBottomContent()));
20845
+ }, onClick: this.clearTextInput }, hAsync("ion-icon", { key: '11e2c866135f332e81b4637de178e1c5c7da0d38', "aria-hidden": "true", icon: clearIconData }))), hAsync("slot", { key: 'afb0e75e7103b2bf925a9c7649c93eb1d5702670', name: "end" })), shouldRenderHighlight && hAsync("div", { key: '1f305bba279e93685d7139acf0aff6bf4a77500c', class: "input-highlight" })), this.renderBottomContent()));
20782
20846
  }
20783
20847
  get el() { return getElement(this); }
20784
20848
  static get watchers() { return {
@@ -37943,41 +38007,21 @@ class Select {
37943
38007
  */
37944
38008
  this.required = false;
37945
38009
  this.onClick = (ev) => {
37946
- const target = ev.target;
37947
- const closestSlot = target.closest('[slot="start"], [slot="end"]');
37948
- if (target === this.el || closestSlot === null) {
37949
- this.setFocus();
37950
- this.open(ev);
37951
- }
37952
- else {
37953
- /**
37954
- * Prevent clicks to the start/end slots from opening the select.
37955
- * We ensure the target isn't this element in case the select is slotted
37956
- * in, for example, an item. This would prevent the select from ever
37957
- * being opened since the element itself has slot="start"/"end".
37958
- *
37959
- * Clicking a slotted element also causes a click
37960
- * on the <label> element (since it wraps the slots).
37961
- * Clicking <label> dispatches another click event on
37962
- * the native form control that then bubbles up to this
37963
- * listener. This additional event targets the host
37964
- * element, so the select overlay is opened.
37965
- *
37966
- * When the slotted elements are clicked (and therefore
37967
- * the ancestor <label> element) we want to prevent the label
37968
- * from dispatching another click event.
37969
- *
37970
- * Do not call stopPropagation() because this will cause
37971
- * click handlers on the slotted elements to never fire in React.
37972
- * When developers do onClick in React a native "click" listener
37973
- * is added on the root element, not the slotted element. When that
37974
- * native click listener fires, React then dispatches the synthetic
37975
- * click event on the slotted element. However, if stopPropagation
37976
- * is called then the native click event will never bubble up
37977
- * to the root element.
37978
- */
37979
- ev.preventDefault();
38010
+ const slotted = getSlottedClickContent(ev, this.el);
38011
+ /**
38012
+ * Interactive slotted content, such as a button or a checkbox, handles its
38013
+ * own click, so it should not open the select as well. Any other slotted
38014
+ * content is decorative and behaves the same as clicking the select
38015
+ * itself.
38016
+ */
38017
+ if (slotted !== null) {
38018
+ const interactive = ev.target.closest(INTERACTIVE_SLOTTED_CONTENT);
38019
+ if (interactive !== null && slotted.contains(interactive)) {
38020
+ return;
38021
+ }
37980
38022
  }
38023
+ this.setFocus();
38024
+ this.open(ev);
37981
38025
  };
37982
38026
  this.onFocus = () => {
37983
38027
  this.hasFocus = true;
@@ -38047,6 +38091,7 @@ class Select {
38047
38091
  return Build.isBrowser;
38048
38092
  });
38049
38093
  this.startContainerController.calculateStartContainerWidth();
38094
+ this.clickController = createClickController(el);
38050
38095
  this.updateOverlayOptions();
38051
38096
  this.emitStyle();
38052
38097
  this.mutationO = watchForOptions(this.el, 'ion-select-option', async () => {
@@ -38504,6 +38549,18 @@ class Select {
38504
38549
  };
38505
38550
  this.ionStyle.emit(style);
38506
38551
  }
38552
+ /**
38553
+ * The label wrapping the slots has no `for` attribute, so the browser
38554
+ * forwards a click on slotted content to the label's first labelable
38555
+ * descendant, the internal button. That forwarded click bubbles back out of
38556
+ * the shadow root targeting the host, where it would be emitted a second
38557
+ * time and open the select. The controller swallows it during the capture
38558
+ * phase, leaving the click on the slotted content itself alone so slotted
38559
+ * links, checkboxes and buttons keep their default behavior.
38560
+ */
38561
+ onClickCapture(ev) {
38562
+ this.clickController?.handleClickCapture(ev);
38563
+ }
38507
38564
  renderLabel() {
38508
38565
  const { label } = this;
38509
38566
  return (hAsync("div", { class: {
@@ -38701,7 +38758,7 @@ class Select {
38701
38758
  const hasOutlineFill = mode === 'md' && fill === 'outline';
38702
38759
  renderHiddenInput(true, el, name, parseValue(value), disabled);
38703
38760
  const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || hasFocus || isExpanded));
38704
- return (hAsync(Host, { key: 'b2f161e3706e139e61440cd13e162714b1b75d72', onClick: this.onClick, class: createColorClasses$1(this.color, {
38761
+ return (hAsync(Host, { key: '31585557ac49c439939b5a084867f5039dc2f1ce', onClick: this.onClick, class: createColorClasses$1(this.color, {
38705
38762
  [mode]: true,
38706
38763
  'in-item': inItem,
38707
38764
  'in-item-color': hostContext('ion-item.ion-color', el),
@@ -38719,7 +38776,7 @@ class Select {
38719
38776
  [`select-justify-${justify}`]: justifyEnabled,
38720
38777
  [`select-shape-${shape}`]: shape !== undefined,
38721
38778
  [`select-label-placement-${labelPlacement}`]: true,
38722
- }) }, hAsync("label", { key: '944a2eb468154a21260d181fdaf67828a71eb902', class: "select-wrapper", id: "select-label", onClick: this.onLabelClick, part: "wrapper" }, hasOutlineFill && this.renderOutlineContainer(), hAsync("div", { key: '927200c53548b1bb4f52d6a167605b724a7b7d0d', class: "select-start", part: "start", ref: (el) => (this.startContainerEl = el) }, hAsync("slot", { key: '6e14b95e9b95c116b16744d630600e90977782bc', name: "start" })), hAsync("div", { key: '8451ed4300ac005963075be36a04ea8763bc0fdd', class: "select-control", part: "control" }, this.renderLabel(), hAsync("div", { key: '207e7b63f3224a4870579365ac992da6784fb31f', class: "native-wrapper", ref: (el) => (this.nativeWrapperEl = el), part: "container" }, this.renderSelectText(), this.renderListbox(), !hasFloatingOrStackedLabel && this.renderSelectIcon())), hAsync("div", { key: '5c17367d488db4d570f31b25e33560efa9011500', class: "select-end", part: "end" }, hasFloatingOrStackedLabel && this.renderSelectIcon(), hAsync("slot", { key: '21fd8c0d9bee8cffef9ef6984cacd31e8f38211c', name: "end" })), shouldRenderHighlight && hAsync("div", { key: '1fff767db9a1ec07b274ef65ff7ae8fb976e6ad2', class: "select-highlight" })), this.renderBottomContent()));
38779
+ }) }, hAsync("label", { key: '368f5f0c77550be6a743752dd1acbfe9915f0a3d', class: "select-wrapper", id: "select-label", onClick: this.onLabelClick, part: "wrapper" }, hasOutlineFill && this.renderOutlineContainer(), hAsync("div", { key: '2e9039eaa15f398ebe54808ff659db89d3621681', class: "select-start", part: "start", ref: (el) => (this.startContainerEl = el) }, hAsync("slot", { key: 'b83e362f89bd8cbc564ad7122778e3a0f08be66d', name: "start" })), hAsync("div", { key: 'f55ac9fc3bbd9a4a3819a101a3780fc02ae8a8e0', class: "select-control", part: "control" }, this.renderLabel(), hAsync("div", { key: 'e32984e57cda5cf341af8e354a2aef5a114827c0', class: "native-wrapper", ref: (el) => (this.nativeWrapperEl = el), part: "container" }, this.renderSelectText(), this.renderListbox(), !hasFloatingOrStackedLabel && this.renderSelectIcon())), hAsync("div", { key: 'fc4cb7c392c46129afe709d9557ad9a10d72b564', class: "select-end", part: "end" }, hasFloatingOrStackedLabel && this.renderSelectIcon(), hAsync("slot", { key: '6d65c92cdafe985a769fb3b41a58625077803eef', name: "end" })), shouldRenderHighlight && hAsync("div", { key: '9a912c64d7cd77f074a069d126cd0bada1b71eed', class: "select-highlight" })), this.renderBottomContent()));
38723
38780
  }
38724
38781
  get el() { return getElement(this); }
38725
38782
  static get watchers() { return {
@@ -38772,7 +38829,7 @@ class Select {
38772
38829
  "hintTextId": [32],
38773
38830
  "open": [64]
38774
38831
  },
38775
- "$listeners$": undefined,
38832
+ "$listeners$": [[2, "click", "onClickCapture"]],
38776
38833
  "$lazyBundleId$": "-",
38777
38834
  "$attrsToReflect$": [["color", "color"]]
38778
38835
  }; }
@@ -39016,6 +39073,33 @@ const extractOptionContent = (option, customHTMLEnabled) => {
39016
39073
  };
39017
39074
  let selectIds = 0;
39018
39075
  const OPTION_CLASS = 'select-interface-option';
39076
+ /**
39077
+ * Slotted content that handles its own click, so clicking it should not also
39078
+ * open the select.
39079
+ *
39080
+ * This deliberately does not reuse `focusableQueryString`. That selector
39081
+ * answers whether an element can take focus right now, which is a different
39082
+ * question: an ion-radio outside a radio group carries tabindex="-1" from the
39083
+ * group's roving tabindex, and disabled controls are excluded, yet both still
39084
+ * handle their own clicks.
39085
+ */
39086
+ const INTERACTIVE_SLOTTED_CONTENT = [
39087
+ 'a[href]',
39088
+ 'button',
39089
+ 'input[type="checkbox"]',
39090
+ 'input[type="radio"]',
39091
+ 'ion-button',
39092
+ 'ion-checkbox',
39093
+ 'ion-radio',
39094
+ 'ion-toggle',
39095
+ '[tabindex]:not([tabindex^="-"])',
39096
+ /**
39097
+ * Covers the remaining Ionic controls. The tags above are still listed
39098
+ * because ion-checkbox and ion-radio only carry this class when they are
39099
+ * outside an item, so a select inside an item would lose the match.
39100
+ */
39101
+ '.ion-focusable',
39102
+ ].join(', ');
39019
39103
 
39020
39104
  const ionicSelectModalMdCss = () => `.action-sheet-button-label-has-rich-content.sc-ion-select-modal-ionic,.alert-radio-label-has-rich-content.sc-ion-select-modal-ionic,.alert-checkbox-label-has-rich-content.sc-ion-select-modal-ionic,.select-option-label-has-rich-content.sc-ion-select-modal-ionic{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;gap:16px}.action-sheet-button-label-has-rich-content.sc-ion-select-modal-ionic,.alert-radio-label-has-rich-content.sc-ion-select-modal-ionic,.alert-checkbox-label-has-rich-content.sc-ion-select-modal-ionic,.select-option-content.sc-ion-select-modal-ionic{-ms-flex:1;flex:1}.action-sheet-button-label-text.sc-ion-select-modal-ionic,.alert-checkbox-label-text.sc-ion-select-modal-ionic,.alert-radio-label-text.sc-ion-select-modal-ionic,.select-option-label-text.sc-ion-select-modal-ionic{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;gap:12px}.select-option-start.sc-ion-select-modal-ionic,.select-option-end.sc-ion-select-modal-ionic{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;gap:8px}.select-option-description.sc-ion-select-modal-ionic{padding-left:0;padding-right:0;padding-top:5px;padding-bottom:0;display:block;color:var(--ion-color-step-700, var(--ion-text-color-step-300, #4d4d4d));font-size:0.75rem}.select-option-label.sc-ion-select-modal-ionic:not(.select-option-label-has-rich-content){text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.select-option-label-has-rich-content.sc-ion-select-modal-ionic{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}ion-radio.select-option-has-rich-content.sc-ion-select-modal-ionic::part(label),ion-radio.select-option-has-rich-content.sc-ion-select-modal-ionic [part~="label"],ion-checkbox.select-option-has-rich-content.sc-ion-select-modal-ionic::part(label),ion-checkbox.select-option-has-rich-content.sc-ion-select-modal-ionic [part~="label"],.select-option-content.sc-ion-select-modal-ionic{-ms-flex:1;flex:1;white-space:normal}.select-option-start.sc-ion-select-modal-ionic>ion-avatar.sc-ion-select-modal-ionic,.select-option-end.sc-ion-select-modal-ionic>ion-avatar.sc-ion-select-modal-ionic{width:40px;height:40px}.select-option-start.sc-ion-select-modal-ionic>ion-icon.sc-ion-select-modal-ionic,.select-option-end.sc-ion-select-modal-ionic>ion-icon.sc-ion-select-modal-ionic{font-size:24px}.select-option-start.sc-ion-select-modal-ionic>ion-img.sc-ion-select-modal-ionic,.select-option-start.sc-ion-select-modal-ionic>img.sc-ion-select-modal-ionic,.select-option-start.sc-ion-select-modal-ionic>svg.sc-ion-select-modal-ionic,.select-option-start.sc-ion-select-modal-ionic>ion-thumbnail.sc-ion-select-modal-ionic,.select-option-end.sc-ion-select-modal-ionic>ion-img.sc-ion-select-modal-ionic,.select-option-end.sc-ion-select-modal-ionic>img.sc-ion-select-modal-ionic,.select-option-end.sc-ion-select-modal-ionic>svg.sc-ion-select-modal-ionic,.select-option-end.sc-ion-select-modal-ionic>ion-thumbnail.sc-ion-select-modal-ionic{width:56px;height:56px}.select-option-start.sc-ion-select-modal-ionic>video.sc-ion-select-modal-ionic,.select-option-end.sc-ion-select-modal-ionic>video.sc-ion-select-modal-ionic{width:114px;height:56px}.sc-ion-select-modal-ionic-h{height:100%}ion-list.sc-ion-select-modal-ionic ion-radio.sc-ion-select-modal-ionic::part(container),ion-list.sc-ion-select-modal-ionic ion-radio.sc-ion-select-modal-ionic [part~="container"]{display:none}ion-list.sc-ion-select-modal-ionic ion-radio.sc-ion-select-modal-ionic::part(label),ion-list.sc-ion-select-modal-ionic ion-radio.sc-ion-select-modal-ionic [part~="label"]{margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}ion-item.sc-ion-select-modal-ionic{--inner-border-width:0}.item-radio-checked.sc-ion-select-modal-ionic{--background:rgba(var(--ion-color-primary-rgb, 0, 84, 233), 0.08);--background-focused:var(--ion-color-primary, #0054e9);--background-focused-opacity:0.2;--background-hover:var(--ion-color-primary, #0054e9);--background-hover-opacity:0.12}.item-checkbox-checked.sc-ion-select-modal-ionic{--background-activated:var(--ion-item-color, var(--ion-text-color, #000));--background-focused:var(--ion-item-color, var(--ion-text-color, #000));--background-hover:var(--ion-item-color, var(--ion-text-color, #000));--color:var(--ion-color-primary, #0054e9)}`;
39021
39105
 
@@ -40338,11 +40422,7 @@ class Textarea {
40338
40422
  * Instead, the click event from the ion-textarea is emitted.
40339
40423
  */
40340
40424
  onClickCapture(ev) {
40341
- const nativeInput = this.nativeInput;
40342
- if (nativeInput && ev.target === nativeInput) {
40343
- ev.stopPropagation();
40344
- this.el.click();
40345
- }
40425
+ this.clickController?.handleClickCapture(ev);
40346
40426
  }
40347
40427
  connectedCallback() {
40348
40428
  const { el } = this;
@@ -40354,6 +40434,7 @@ class Textarea {
40354
40434
  return Build.isBrowser;
40355
40435
  });
40356
40436
  this.startContainerController.calculateStartContainerWidth();
40437
+ this.clickController = createClickController(el, () => this.nativeInput);
40357
40438
  // Always set initial state
40358
40439
  this.isInvalid = checkInvalidState(this.el);
40359
40440
  this.debounceChanged();
@@ -40584,7 +40665,7 @@ class Textarea {
40584
40665
  * the textarea has a value or is focused.
40585
40666
  */
40586
40667
  const labelShouldFloat = labelPlacement === 'stacked' || (labelPlacement === 'floating' && (hasValue || hasFocus));
40587
- return (hAsync(Host, { key: '78da9c96e6a29f19b237f268dd8bfd044afe27b6', class: createColorClasses$1(this.color, {
40668
+ return (hAsync(Host, { key: '0f01ed6565313c45beaea5e0aa429a86b9cfa1a0', class: createColorClasses$1(this.color, {
40588
40669
  [mode]: true,
40589
40670
  'has-value': hasValue,
40590
40671
  'has-focus': hasFocus,
@@ -40593,7 +40674,7 @@ class Textarea {
40593
40674
  [`textarea-shape-${shape}`]: shape !== undefined,
40594
40675
  [`textarea-label-placement-${labelPlacement}`]: true,
40595
40676
  'textarea-disabled': disabled,
40596
- }) }, hAsync("label", { key: 'c5ea3f26e01e66b857d3a1c88dbafbf2dbb81fdb', class: "textarea-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, hasOutlineFill && this.renderOutlineContainer(), hAsync("div", { key: '5a9d0efc8c13d9bd4ec4ceadf1bdb84205c47b3d', class: "textarea-start", ref: (el) => (this.startContainerEl = el) }, hAsync("slot", { key: 'da08262e33f3a5b454847297338a74f07c5b60c9', name: "start" })), hAsync("div", { key: '6461dc7381929eb83679ec07563b7bdf8bfc3f9f', class: "textarea-control" }, this.renderLabel(), hAsync("div", { key: '8f7902a9e6040384d25d0e2061c04f6b49acf187', class: "native-wrapper", ref: (el) => (this.textareaWrapper = el) }, hAsync("textarea", { key: '2149d8b2c23a164c536327c98d7a632e1e85428a', class: "native-textarea", ref: (el) => (this.nativeInput = el), id: inputId, disabled: disabled, autoCapitalize: this.autocapitalize, autoFocus: this.autofocus, enterKeyHint: this.enterkeyhint, inputMode: this.inputmode, minLength: this.minlength, maxLength: this.maxlength, name: this.name, placeholder: this.placeholder || '', readOnly: this.readonly, required: this.required, spellcheck: this.spellcheck, cols: this.cols, rows: this.rows, wrap: this.wrap, onInput: this.onInput, onChange: this.onChange, onBlur: this.onBlur, onFocus: this.onFocus, onKeyDown: this.onKeyDown, "aria-describedby": this.getHintTextID(), "aria-invalid": this.isInvalid ? 'true' : undefined, ...this.inheritedAttributes }, value))), hAsync("div", { key: '84f5f42add89875c3f0cce734b512a9e7be81e03', class: "textarea-end" }, hAsync("slot", { key: 'ea654d78013a00b8af33deb89d42c2b7f6c9cb99', name: "end" })), shouldRenderHighlight && hAsync("div", { key: 'b72195d2606816b3b71e65214d78cef1740f4804', class: "textarea-highlight" })), this.renderBottomContent()));
40677
+ }) }, hAsync("label", { key: 'a59548c3d3bf66543fd6e06b5c3b6e649f963164', class: "textarea-wrapper", htmlFor: inputId, onClick: this.onLabelClick }, hasOutlineFill && this.renderOutlineContainer(), hAsync("div", { key: '29055193d664d9fe84ba0340fdcbbc54fec0e403', class: "textarea-start", ref: (el) => (this.startContainerEl = el) }, hAsync("slot", { key: '2cc122d9541ce7ae3c2309c27123b6c6c91cbfc7', name: "start" })), hAsync("div", { key: 'c847a6fa1eaf6ef2b296415cd72bdd3aee9828a2', class: "textarea-control" }, this.renderLabel(), hAsync("div", { key: 'f9205f82d6346daa86dc7e0f6e1eb221c76ed7b0', class: "native-wrapper", ref: (el) => (this.textareaWrapper = el) }, hAsync("textarea", { key: '0bdb517fa5b3a44eeb3bb2ed1f9a8eab21a9a6a1', class: "native-textarea", ref: (el) => (this.nativeInput = el), id: inputId, disabled: disabled, autoCapitalize: this.autocapitalize, autoFocus: this.autofocus, enterKeyHint: this.enterkeyhint, inputMode: this.inputmode, minLength: this.minlength, maxLength: this.maxlength, name: this.name, placeholder: this.placeholder || '', readOnly: this.readonly, required: this.required, spellcheck: this.spellcheck, cols: this.cols, rows: this.rows, wrap: this.wrap, onInput: this.onInput, onChange: this.onChange, onBlur: this.onBlur, onFocus: this.onFocus, onKeyDown: this.onKeyDown, "aria-describedby": this.getHintTextID(), "aria-invalid": this.isInvalid ? 'true' : undefined, ...this.inheritedAttributes }, value))), hAsync("div", { key: '1ce661b800ef12146bdb97fb193e22be227ea052', class: "textarea-end" }, hAsync("slot", { key: 'aca5704db48fe8e0e784cf57f2978ae2d63b3edc', name: "end" })), shouldRenderHighlight && hAsync("div", { key: 'd916816d44cbdb4bc9a05032c5490eaa9bf3ba47', class: "textarea-highlight" })), this.renderBottomContent()));
40597
40678
  }
40598
40679
  get el() { return getElement(this); }
40599
40680
  static get watchers() { return {