select.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  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.97
  5. */
  6. import { Fragment, h } from "@stencil/core";
  7. import { focusElement } from "../../utils/dom";
  8. import { connectLabel, disconnectLabel } from "../../utils/label";
  9. import { afterConnectDefaultValueSet, connectForm, disconnectForm, HiddenFormInputSlot } from "../../utils/form";
  10. import { CSS } from "./resources";
  11. import { createObserver } from "../../utils/observers";
  12. import { updateHostInteraction } from "../../utils/interactive";
  13. function isOption(optionOrGroup) {
  14. return optionOrGroup.tagName === "CALCITE-OPTION";
  15. }
  16. function isOptionGroup(optionOrGroup) {
  17. return optionOrGroup.tagName === "CALCITE-OPTION-GROUP";
  18. }
  19. /**
  20. * @slot - A slot for adding `calcite-option`s.
  21. */
  22. export class Select {
  23. constructor() {
  24. //--------------------------------------------------------------------------
  25. //
  26. // Properties
  27. //
  28. //--------------------------------------------------------------------------
  29. /**
  30. * When `true`, interaction is prevented and the component is displayed with lower opacity.
  31. */
  32. this.disabled = false;
  33. /**
  34. * When `true`, the component must have a value in order for the form to submit.
  35. *
  36. * @internal
  37. */
  38. this.required = false;
  39. /**
  40. * Specifies the size of the component.
  41. */
  42. this.scale = "m";
  43. /** The component's `selectedOption` value. */
  44. this.value = null;
  45. /**
  46. * Specifies the width of the component.
  47. */
  48. this.width = "auto";
  49. this.componentToNativeEl = new Map();
  50. this.mutationObserver = createObserver("mutation", () => this.populateInternalSelect());
  51. this.handleInternalSelectChange = () => {
  52. const selected = this.selectEl.selectedOptions[0];
  53. this.selectFromNativeOption(selected);
  54. requestAnimationFrame(() => this.emitChangeEvent());
  55. };
  56. this.populateInternalSelect = () => {
  57. const optionsAndGroups = Array.from(this.el.children).filter((child) => child.tagName === "CALCITE-OPTION" || child.tagName === "CALCITE-OPTION-GROUP");
  58. this.clearInternalSelect();
  59. optionsAndGroups.forEach((optionOrGroup) => { var _a; return (_a = this.selectEl) === null || _a === void 0 ? void 0 : _a.append(this.toNativeElement(optionOrGroup)); });
  60. };
  61. this.storeSelectRef = (node) => {
  62. this.selectEl = node;
  63. this.populateInternalSelect();
  64. const selected = this.selectEl.selectedOptions[0];
  65. this.selectFromNativeOption(selected);
  66. };
  67. this.emitChangeEvent = () => {
  68. this.calciteSelectChange.emit();
  69. };
  70. }
  71. valueHandler(value) {
  72. const items = this.el.querySelectorAll("calcite-option");
  73. items.forEach((item) => (item.selected = item.value === value));
  74. }
  75. selectedOptionHandler(selectedOption) {
  76. this.value = selectedOption === null || selectedOption === void 0 ? void 0 : selectedOption.value;
  77. }
  78. //--------------------------------------------------------------------------
  79. //
  80. // Lifecycle
  81. //
  82. //--------------------------------------------------------------------------
  83. connectedCallback() {
  84. var _a;
  85. const { el } = this;
  86. (_a = this.mutationObserver) === null || _a === void 0 ? void 0 : _a.observe(el, {
  87. subtree: true,
  88. childList: true
  89. });
  90. connectLabel(this);
  91. connectForm(this);
  92. }
  93. disconnectedCallback() {
  94. var _a;
  95. (_a = this.mutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
  96. disconnectLabel(this);
  97. disconnectForm(this);
  98. }
  99. componentDidLoad() {
  100. var _a, _b;
  101. afterConnectDefaultValueSet(this, (_b = (_a = this.selectedOption) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : "");
  102. }
  103. componentDidRender() {
  104. updateHostInteraction(this);
  105. }
  106. //--------------------------------------------------------------------------
  107. //
  108. // Public Methods
  109. //
  110. //--------------------------------------------------------------------------
  111. /** Sets focus on the component. */
  112. async setFocus() {
  113. focusElement(this.selectEl);
  114. }
  115. handleOptionOrGroupChange(event) {
  116. event.stopPropagation();
  117. const optionOrGroup = event.target;
  118. const nativeEl = this.componentToNativeEl.get(optionOrGroup);
  119. if (!nativeEl) {
  120. return;
  121. }
  122. this.updateNativeElement(optionOrGroup, nativeEl);
  123. if (isOption(optionOrGroup) && optionOrGroup.selected) {
  124. this.deselectAllExcept(optionOrGroup);
  125. this.selectedOption = optionOrGroup;
  126. }
  127. }
  128. //--------------------------------------------------------------------------
  129. //
  130. // Private Methods
  131. //
  132. //--------------------------------------------------------------------------
  133. onLabelClick() {
  134. this.setFocus();
  135. }
  136. updateNativeElement(optionOrGroup, nativeOptionOrGroup) {
  137. nativeOptionOrGroup.disabled = optionOrGroup.disabled;
  138. nativeOptionOrGroup.label = optionOrGroup.label;
  139. if (isOption(optionOrGroup)) {
  140. const option = nativeOptionOrGroup;
  141. option.selected = optionOrGroup.selected;
  142. option.value = optionOrGroup.value;
  143. // need to set innerText for mobile
  144. // see https://stackoverflow.com/questions/35021620/ios-safari-not-showing-all-options-for-select-menu/41749701
  145. option.innerText = optionOrGroup.label;
  146. }
  147. }
  148. clearInternalSelect() {
  149. this.componentToNativeEl.forEach((value) => value.remove());
  150. this.componentToNativeEl.clear();
  151. }
  152. selectFromNativeOption(nativeOption) {
  153. if (!nativeOption) {
  154. return;
  155. }
  156. let futureSelected;
  157. this.componentToNativeEl.forEach((nativeOptionOrGroup, optionOrGroup) => {
  158. if (isOption(optionOrGroup) && nativeOptionOrGroup === nativeOption) {
  159. optionOrGroup.selected = true;
  160. futureSelected = optionOrGroup;
  161. this.deselectAllExcept(optionOrGroup);
  162. }
  163. });
  164. if (futureSelected) {
  165. this.selectedOption = futureSelected;
  166. }
  167. }
  168. toNativeElement(optionOrGroup) {
  169. if (isOption(optionOrGroup)) {
  170. const option = document.createElement("option");
  171. this.updateNativeElement(optionOrGroup, option);
  172. this.componentToNativeEl.set(optionOrGroup, option);
  173. return option;
  174. }
  175. if (isOptionGroup(optionOrGroup)) {
  176. const group = document.createElement("optgroup");
  177. this.updateNativeElement(optionOrGroup, group);
  178. Array.from(optionOrGroup.children).forEach((option) => {
  179. const nativeOption = this.toNativeElement(option);
  180. group.append(nativeOption);
  181. this.componentToNativeEl.set(optionOrGroup, nativeOption);
  182. });
  183. this.componentToNativeEl.set(optionOrGroup, group);
  184. return group;
  185. }
  186. throw new Error("unsupported element child provided");
  187. }
  188. deselectAllExcept(except) {
  189. this.el.querySelectorAll("calcite-option").forEach((option) => {
  190. if (option === except) {
  191. return;
  192. }
  193. option.selected = false;
  194. });
  195. }
  196. //--------------------------------------------------------------------------
  197. //
  198. // Render Methods
  199. //
  200. //--------------------------------------------------------------------------
  201. renderChevron() {
  202. return (h("div", { class: CSS.iconContainer }, h("calcite-icon", { class: CSS.icon, icon: "chevron-down", scale: "s" })));
  203. }
  204. render() {
  205. return (h(Fragment, null, h("select", { "aria-label": this.label, class: CSS.select, disabled: this.disabled, onChange: this.handleInternalSelectChange, ref: this.storeSelectRef }, h("slot", null)), this.renderChevron(), h(HiddenFormInputSlot, { component: this })));
  206. }
  207. static get is() { return "calcite-select"; }
  208. static get encapsulation() { return "shadow"; }
  209. static get originalStyleUrls() {
  210. return {
  211. "$": ["select.scss"]
  212. };
  213. }
  214. static get styleUrls() {
  215. return {
  216. "$": ["select.css"]
  217. };
  218. }
  219. static get properties() {
  220. return {
  221. "disabled": {
  222. "type": "boolean",
  223. "mutable": false,
  224. "complexType": {
  225. "original": "boolean",
  226. "resolved": "boolean",
  227. "references": {}
  228. },
  229. "required": false,
  230. "optional": false,
  231. "docs": {
  232. "tags": [],
  233. "text": "When `true`, interaction is prevented and the component is displayed with lower opacity."
  234. },
  235. "attribute": "disabled",
  236. "reflect": true,
  237. "defaultValue": "false"
  238. },
  239. "label": {
  240. "type": "string",
  241. "mutable": false,
  242. "complexType": {
  243. "original": "string",
  244. "resolved": "string",
  245. "references": {}
  246. },
  247. "required": true,
  248. "optional": false,
  249. "docs": {
  250. "tags": [],
  251. "text": "Accessible name for the component."
  252. },
  253. "attribute": "label",
  254. "reflect": false
  255. },
  256. "name": {
  257. "type": "string",
  258. "mutable": false,
  259. "complexType": {
  260. "original": "string",
  261. "resolved": "string",
  262. "references": {}
  263. },
  264. "required": false,
  265. "optional": false,
  266. "docs": {
  267. "tags": [],
  268. "text": "Specifies the name of the component on form submission."
  269. },
  270. "attribute": "name",
  271. "reflect": true
  272. },
  273. "required": {
  274. "type": "boolean",
  275. "mutable": false,
  276. "complexType": {
  277. "original": "boolean",
  278. "resolved": "boolean",
  279. "references": {}
  280. },
  281. "required": false,
  282. "optional": false,
  283. "docs": {
  284. "tags": [{
  285. "name": "internal",
  286. "text": undefined
  287. }],
  288. "text": "When `true`, the component must have a value in order for the form to submit."
  289. },
  290. "attribute": "required",
  291. "reflect": true,
  292. "defaultValue": "false"
  293. },
  294. "scale": {
  295. "type": "string",
  296. "mutable": false,
  297. "complexType": {
  298. "original": "Scale",
  299. "resolved": "\"l\" | \"m\" | \"s\"",
  300. "references": {
  301. "Scale": {
  302. "location": "import",
  303. "path": "../interfaces"
  304. }
  305. }
  306. },
  307. "required": false,
  308. "optional": false,
  309. "docs": {
  310. "tags": [],
  311. "text": "Specifies the size of the component."
  312. },
  313. "attribute": "scale",
  314. "reflect": true,
  315. "defaultValue": "\"m\""
  316. },
  317. "value": {
  318. "type": "string",
  319. "mutable": true,
  320. "complexType": {
  321. "original": "string",
  322. "resolved": "string",
  323. "references": {}
  324. },
  325. "required": false,
  326. "optional": false,
  327. "docs": {
  328. "tags": [],
  329. "text": "The component's `selectedOption` value."
  330. },
  331. "attribute": "value",
  332. "reflect": false,
  333. "defaultValue": "null"
  334. },
  335. "selectedOption": {
  336. "type": "unknown",
  337. "mutable": true,
  338. "complexType": {
  339. "original": "HTMLCalciteOptionElement",
  340. "resolved": "HTMLCalciteOptionElement",
  341. "references": {
  342. "HTMLCalciteOptionElement": {
  343. "location": "global"
  344. }
  345. }
  346. },
  347. "required": false,
  348. "optional": false,
  349. "docs": {
  350. "tags": [{
  351. "name": "readonly",
  352. "text": undefined
  353. }],
  354. "text": "The component's selected option `HTMLElement`."
  355. }
  356. },
  357. "width": {
  358. "type": "string",
  359. "mutable": false,
  360. "complexType": {
  361. "original": "Width",
  362. "resolved": "\"auto\" | \"full\" | \"half\"",
  363. "references": {
  364. "Width": {
  365. "location": "import",
  366. "path": "../interfaces"
  367. }
  368. }
  369. },
  370. "required": false,
  371. "optional": false,
  372. "docs": {
  373. "tags": [],
  374. "text": "Specifies the width of the component."
  375. },
  376. "attribute": "width",
  377. "reflect": true,
  378. "defaultValue": "\"auto\""
  379. }
  380. };
  381. }
  382. static get events() {
  383. return [{
  384. "method": "calciteSelectChange",
  385. "name": "calciteSelectChange",
  386. "bubbles": true,
  387. "cancelable": false,
  388. "composed": true,
  389. "docs": {
  390. "tags": [],
  391. "text": "Fires when the `selectedOption` changes."
  392. },
  393. "complexType": {
  394. "original": "void",
  395. "resolved": "void",
  396. "references": {}
  397. }
  398. }];
  399. }
  400. static get methods() {
  401. return {
  402. "setFocus": {
  403. "complexType": {
  404. "signature": "() => Promise<void>",
  405. "parameters": [],
  406. "references": {
  407. "Promise": {
  408. "location": "global"
  409. }
  410. },
  411. "return": "Promise<void>"
  412. },
  413. "docs": {
  414. "text": "Sets focus on the component.",
  415. "tags": []
  416. }
  417. }
  418. };
  419. }
  420. static get elementRef() { return "el"; }
  421. static get watchers() {
  422. return [{
  423. "propName": "value",
  424. "methodName": "valueHandler"
  425. }, {
  426. "propName": "selectedOption",
  427. "methodName": "selectedOptionHandler"
  428. }];
  429. }
  430. static get listeners() {
  431. return [{
  432. "name": "calciteInternalOptionChange",
  433. "method": "handleOptionOrGroupChange",
  434. "target": undefined,
  435. "capture": false,
  436. "passive": false
  437. }, {
  438. "name": "calciteInternalOptionGroupChange",
  439. "method": "handleOptionOrGroupChange",
  440. "target": undefined,
  441. "capture": false,
  442. "passive": false
  443. }];
  444. }
  445. }