label.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. /*!
  2. * All material copyright ESRI, All Rights Reserved, unless otherwise specified.
  3. * See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details.
  4. * v1.0.0-beta.82
  5. */
  6. import { closestElementCrossShadowBoundary, queryElementRoots } from "./dom";
  7. /**
  8. * Exported for testing purposes only
  9. * @internal
  10. */
  11. export const labelClickEvent = "calciteInternalLabelClick";
  12. export const labelConnectedEvent = "calciteInternalLabelConnected";
  13. export const labelDisconnectedEvent = "calciteInternaLabelDisconnected";
  14. const labelTagName = "calcite-label";
  15. const onLabelClickMap = new WeakMap();
  16. const onLabelConnectedMap = new WeakMap();
  17. const onLabelDisconnectedMap = new WeakMap();
  18. const unlabeledComponents = new Set();
  19. const findLabelForComponent = (componentEl) => {
  20. const { id } = componentEl;
  21. const forLabel = id && queryElementRoots(componentEl, { selector: `${labelTagName}[for="${id}"]` });
  22. if (forLabel) {
  23. return forLabel;
  24. }
  25. const parentLabel = closestElementCrossShadowBoundary(componentEl, labelTagName);
  26. if (!parentLabel ||
  27. // labelable components within other custom elements are not considered labelable
  28. hasAncestorCustomElements(parentLabel, componentEl)) {
  29. return null;
  30. }
  31. return parentLabel;
  32. };
  33. function hasAncestorCustomElements(label, componentEl) {
  34. let traversedElements;
  35. const customElementAncestorCheckEventType = "custom-element-ancestor-check";
  36. const listener = (event) => {
  37. event.stopImmediatePropagation();
  38. const composedPath = event.composedPath();
  39. traversedElements = composedPath.slice(composedPath.indexOf(componentEl), composedPath.indexOf(label));
  40. };
  41. label.addEventListener(customElementAncestorCheckEventType, listener, { once: true });
  42. componentEl.dispatchEvent(new CustomEvent(customElementAncestorCheckEventType, { composed: true, bubbles: true }));
  43. label.removeEventListener(customElementAncestorCheckEventType, listener);
  44. const ancestorCustomElements = traversedElements
  45. .filter((el) => el !== componentEl && el !== label)
  46. .filter((el) => { var _a; return (_a = el.tagName) === null || _a === void 0 ? void 0 : _a.includes("-"); });
  47. return ancestorCustomElements.length > 0;
  48. }
  49. /**
  50. * Helper to set up label interactions on connectedCallback.
  51. */
  52. export function connectLabel(component) {
  53. const labelEl = findLabelForComponent(component.el);
  54. if (onLabelClickMap.has(labelEl) || (!labelEl && unlabeledComponents.has(component))) {
  55. return;
  56. }
  57. const boundOnLabelDisconnected = onLabelDisconnected.bind(component);
  58. if (labelEl) {
  59. component.labelEl = labelEl;
  60. const boundOnLabelClick = onLabelClick.bind(component);
  61. onLabelClickMap.set(component.labelEl, boundOnLabelClick);
  62. component.labelEl.addEventListener(labelClickEvent, boundOnLabelClick);
  63. unlabeledComponents.delete(component);
  64. document.removeEventListener(labelConnectedEvent, onLabelConnectedMap.get(component));
  65. onLabelDisconnectedMap.set(component, boundOnLabelDisconnected);
  66. document.addEventListener(labelDisconnectedEvent, boundOnLabelDisconnected);
  67. }
  68. else if (!unlabeledComponents.has(component)) {
  69. boundOnLabelDisconnected();
  70. document.removeEventListener(labelDisconnectedEvent, onLabelDisconnectedMap.get(component));
  71. }
  72. }
  73. /**
  74. * Helper to tear down label interactions on disconnectedCallback on labelable components.
  75. */
  76. export function disconnectLabel(component) {
  77. unlabeledComponents.delete(component);
  78. document.removeEventListener(labelConnectedEvent, onLabelConnectedMap.get(component));
  79. document.removeEventListener(labelDisconnectedEvent, onLabelDisconnectedMap.get(component));
  80. onLabelConnectedMap.delete(component);
  81. onLabelDisconnectedMap.delete(component);
  82. if (!component.labelEl) {
  83. return;
  84. }
  85. const boundOnLabelClick = onLabelClickMap.get(component.labelEl);
  86. component.labelEl.removeEventListener(labelClickEvent, boundOnLabelClick);
  87. onLabelClickMap.delete(component.labelEl);
  88. }
  89. /**
  90. * Helper to get the label text from a component.
  91. */
  92. export function getLabelText(component) {
  93. var _a, _b;
  94. return component.label || ((_b = (_a = component.labelEl) === null || _a === void 0 ? void 0 : _a.textContent) === null || _b === void 0 ? void 0 : _b.trim()) || "";
  95. }
  96. function onLabelClick(event) {
  97. if (this.disabled) {
  98. return;
  99. }
  100. const containedLabelableChildClicked = this.el.contains(event.detail.sourceEvent.target);
  101. if (containedLabelableChildClicked) {
  102. return;
  103. }
  104. this.onLabelClick(event);
  105. }
  106. function onLabelConnected() {
  107. if (unlabeledComponents.has(this)) {
  108. connectLabel(this);
  109. }
  110. }
  111. function onLabelDisconnected() {
  112. unlabeledComponents.add(this);
  113. const boundOnLabelConnected = onLabelConnectedMap.get(this) || onLabelConnected.bind(this);
  114. onLabelConnectedMap.set(this, boundOnLabelConnected);
  115. document.addEventListener(labelConnectedEvent, boundOnLabelConnected);
  116. }