button.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  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 "form-request-submit-polyfill/form-request-submit-polyfill";
  7. import { Component, Element, h, Method, Prop, Build, State, Watch } from "@stencil/core";
  8. import { CSS, TEXT } from "./resources";
  9. import { closestElementCrossShadowBoundary } from "../../utils/dom";
  10. import { connectLabel, disconnectLabel, getLabelText } from "../../utils/label";
  11. import { createObserver } from "../../utils/observers";
  12. import { updateHostInteraction } from "../../utils/interactive";
  13. import { submitForm, resetForm } from "../../utils/form";
  14. /** Passing a 'href' will render an anchor link, instead of a button. Role will be set to link, or button, depending on this. */
  15. /** It is the consumers responsibility to add aria information, rel, target, for links, and any button attributes for form submission */
  16. /** @slot - A slot for adding text. */
  17. export class Button {
  18. constructor() {
  19. //--------------------------------------------------------------------------
  20. //
  21. // Properties
  22. //
  23. //--------------------------------------------------------------------------
  24. /** optionally specify alignment of button elements. */
  25. this.alignment = "center";
  26. /** specify the appearance style of the button, defaults to solid. */
  27. this.appearance = "solid";
  28. /** specify the color of the button, defaults to blue */
  29. this.color = "blue";
  30. /** is the button disabled */
  31. this.disabled = false;
  32. /** string to override English loading text
  33. * @default "Loading"
  34. */
  35. this.intlLoading = TEXT.loading;
  36. /** optionally add a calcite-loader component to the button, disabling interaction. */
  37. this.loading = false;
  38. /** optionally add a round style to the button */
  39. this.round = false;
  40. /** specify the scale of the button, defaults to m */
  41. this.scale = "m";
  42. /** is the button a child of a calcite-split-button */
  43. this.splitChild = false;
  44. /** The type attribute to apply to the button */
  45. this.type = "button";
  46. /** specify the width of the button, defaults to auto */
  47. this.width = "auto";
  48. /** watches for changing text content **/
  49. this.mutationObserver = createObserver("mutation", () => this.updateHasContent());
  50. /** determine if there is slotted content for styling purposes */
  51. this.hasContent = false;
  52. /** determine if loader present for styling purposes */
  53. this.hasLoader = false;
  54. // act on a requested or nearby form based on type
  55. this.handleClick = () => {
  56. const { type } = this;
  57. if (this.href) {
  58. return;
  59. }
  60. // this.type refers to type attribute, not child element type
  61. if (type === "submit") {
  62. submitForm(this);
  63. }
  64. else if (type === "reset") {
  65. resetForm(this);
  66. }
  67. };
  68. }
  69. loadingChanged(newValue, oldValue) {
  70. if (!!newValue && !oldValue) {
  71. this.hasLoader = true;
  72. }
  73. if (!newValue && !!oldValue) {
  74. window.setTimeout(() => {
  75. this.hasLoader = false;
  76. }, 300);
  77. }
  78. }
  79. //--------------------------------------------------------------------------
  80. //
  81. // Lifecycle
  82. //
  83. //--------------------------------------------------------------------------
  84. connectedCallback() {
  85. this.hasLoader = this.loading;
  86. this.setupTextContentObserver();
  87. connectLabel(this);
  88. this.formEl = closestElementCrossShadowBoundary(this.el, this.form ? `#${this.form}` : "form");
  89. }
  90. disconnectedCallback() {
  91. var _a;
  92. (_a = this.mutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
  93. disconnectLabel(this);
  94. this.formEl = null;
  95. }
  96. componentWillLoad() {
  97. if (Build.isBrowser) {
  98. this.updateHasContent();
  99. }
  100. }
  101. componentDidRender() {
  102. updateHostInteraction(this);
  103. }
  104. render() {
  105. const childElType = this.href ? "a" : "button";
  106. const Tag = childElType;
  107. const loaderNode = this.hasLoader ? (h("div", { class: CSS.buttonLoader },
  108. h("calcite-loader", { active: true, class: this.loading ? CSS.loadingIn : CSS.loadingOut, inline: true, label: this.intlLoading, scale: "m" }))) : null;
  109. const iconStartEl = (h("calcite-icon", { class: { [CSS.icon]: true, [CSS.iconStart]: true }, flipRtl: this.iconFlipRtl === "start" || this.iconFlipRtl === "both", icon: this.iconStart, scale: "s" }));
  110. const iconEndEl = (h("calcite-icon", { class: { [CSS.icon]: true, [CSS.iconEnd]: true }, flipRtl: this.iconFlipRtl === "end" || this.iconFlipRtl === "both", icon: this.iconEnd, scale: "s" }));
  111. const contentEl = (h("span", { class: CSS.content },
  112. h("slot", null)));
  113. return (h(Tag, { "aria-label": getLabelText(this), class: {
  114. [CSS.contentSlotted]: this.hasContent,
  115. [CSS.iconStartEmpty]: !this.iconStart,
  116. [CSS.iconEndEmpty]: !this.iconEnd
  117. }, disabled: this.disabled || this.loading, href: childElType === "a" && this.href, name: childElType === "button" && this.name, onClick: this.handleClick, ref: (el) => (this.childEl = el), rel: childElType === "a" && this.rel, tabIndex: this.disabled || this.loading ? -1 : null, target: childElType === "a" && this.target, type: childElType === "button" && this.type },
  118. loaderNode,
  119. this.iconStart ? iconStartEl : null,
  120. this.hasContent ? contentEl : null,
  121. this.iconEnd ? iconEndEl : null));
  122. }
  123. //--------------------------------------------------------------------------
  124. //
  125. // Public Methods
  126. //
  127. //--------------------------------------------------------------------------
  128. /** Sets focus on the component. */
  129. async setFocus() {
  130. this.childEl.focus();
  131. }
  132. updateHasContent() {
  133. var _a, _b;
  134. const slottedContent = this.el.textContent.trim().length > 0 || this.el.childNodes.length > 0;
  135. this.hasContent =
  136. this.el.childNodes.length === 1 && ((_a = this.el.childNodes[0]) === null || _a === void 0 ? void 0 : _a.nodeName) === "#text"
  137. ? ((_b = this.el.textContent) === null || _b === void 0 ? void 0 : _b.trim().length) > 0
  138. : slottedContent;
  139. }
  140. setupTextContentObserver() {
  141. var _a;
  142. (_a = this.mutationObserver) === null || _a === void 0 ? void 0 : _a.observe(this.el, { childList: true, subtree: true });
  143. }
  144. //--------------------------------------------------------------------------
  145. //
  146. // Private Methods
  147. //
  148. //--------------------------------------------------------------------------
  149. onLabelClick() {
  150. this.handleClick();
  151. this.setFocus();
  152. }
  153. static get is() { return "calcite-button"; }
  154. static get encapsulation() { return "shadow"; }
  155. static get originalStyleUrls() { return {
  156. "$": ["button.scss"]
  157. }; }
  158. static get styleUrls() { return {
  159. "$": ["button.css"]
  160. }; }
  161. static get properties() { return {
  162. "alignment": {
  163. "type": "string",
  164. "mutable": false,
  165. "complexType": {
  166. "original": "ButtonAlignment",
  167. "resolved": "\"center\" | \"end\" | \"icon-end-space-between\" | \"icon-start-space-between\" | \"space-between\" | \"start\"",
  168. "references": {
  169. "ButtonAlignment": {
  170. "location": "import",
  171. "path": "./interfaces"
  172. }
  173. }
  174. },
  175. "required": false,
  176. "optional": true,
  177. "docs": {
  178. "tags": [],
  179. "text": "optionally specify alignment of button elements."
  180. },
  181. "attribute": "alignment",
  182. "reflect": true,
  183. "defaultValue": "\"center\""
  184. },
  185. "appearance": {
  186. "type": "string",
  187. "mutable": false,
  188. "complexType": {
  189. "original": "ButtonAppearance",
  190. "resolved": "\"clear\" | \"outline\" | \"solid\" | \"transparent\"",
  191. "references": {
  192. "ButtonAppearance": {
  193. "location": "import",
  194. "path": "./interfaces"
  195. }
  196. }
  197. },
  198. "required": false,
  199. "optional": false,
  200. "docs": {
  201. "tags": [],
  202. "text": "specify the appearance style of the button, defaults to solid."
  203. },
  204. "attribute": "appearance",
  205. "reflect": true,
  206. "defaultValue": "\"solid\""
  207. },
  208. "label": {
  209. "type": "string",
  210. "mutable": false,
  211. "complexType": {
  212. "original": "string",
  213. "resolved": "string",
  214. "references": {}
  215. },
  216. "required": false,
  217. "optional": true,
  218. "docs": {
  219. "tags": [],
  220. "text": "Applies to the aria-label attribute on the button or hyperlink"
  221. },
  222. "attribute": "label",
  223. "reflect": false
  224. },
  225. "color": {
  226. "type": "string",
  227. "mutable": false,
  228. "complexType": {
  229. "original": "ButtonColor",
  230. "resolved": "\"blue\" | \"inverse\" | \"neutral\" | \"red\"",
  231. "references": {
  232. "ButtonColor": {
  233. "location": "import",
  234. "path": "./interfaces"
  235. }
  236. }
  237. },
  238. "required": false,
  239. "optional": false,
  240. "docs": {
  241. "tags": [],
  242. "text": "specify the color of the button, defaults to blue"
  243. },
  244. "attribute": "color",
  245. "reflect": true,
  246. "defaultValue": "\"blue\""
  247. },
  248. "disabled": {
  249. "type": "boolean",
  250. "mutable": false,
  251. "complexType": {
  252. "original": "boolean",
  253. "resolved": "boolean",
  254. "references": {}
  255. },
  256. "required": false,
  257. "optional": false,
  258. "docs": {
  259. "tags": [],
  260. "text": "is the button disabled"
  261. },
  262. "attribute": "disabled",
  263. "reflect": true,
  264. "defaultValue": "false"
  265. },
  266. "href": {
  267. "type": "string",
  268. "mutable": false,
  269. "complexType": {
  270. "original": "string",
  271. "resolved": "string",
  272. "references": {}
  273. },
  274. "required": false,
  275. "optional": true,
  276. "docs": {
  277. "tags": [],
  278. "text": "optionally pass a href - used to determine if the component should render as a button or an anchor"
  279. },
  280. "attribute": "href",
  281. "reflect": true
  282. },
  283. "iconEnd": {
  284. "type": "string",
  285. "mutable": false,
  286. "complexType": {
  287. "original": "string",
  288. "resolved": "string",
  289. "references": {}
  290. },
  291. "required": false,
  292. "optional": true,
  293. "docs": {
  294. "tags": [],
  295. "text": "optionally pass an icon to display at the end of a button - accepts calcite ui icon names"
  296. },
  297. "attribute": "icon-end",
  298. "reflect": true
  299. },
  300. "iconFlipRtl": {
  301. "type": "string",
  302. "mutable": false,
  303. "complexType": {
  304. "original": "FlipContext",
  305. "resolved": "\"both\" | \"end\" | \"start\"",
  306. "references": {
  307. "FlipContext": {
  308. "location": "import",
  309. "path": "../interfaces"
  310. }
  311. }
  312. },
  313. "required": false,
  314. "optional": true,
  315. "docs": {
  316. "tags": [],
  317. "text": "flip the icon(s) in rtl"
  318. },
  319. "attribute": "icon-flip-rtl",
  320. "reflect": true
  321. },
  322. "iconStart": {
  323. "type": "string",
  324. "mutable": false,
  325. "complexType": {
  326. "original": "string",
  327. "resolved": "string",
  328. "references": {}
  329. },
  330. "required": false,
  331. "optional": true,
  332. "docs": {
  333. "tags": [],
  334. "text": "optionally pass an icon to display at the start of a button - accepts calcite ui icon names"
  335. },
  336. "attribute": "icon-start",
  337. "reflect": true
  338. },
  339. "intlLoading": {
  340. "type": "string",
  341. "mutable": false,
  342. "complexType": {
  343. "original": "string",
  344. "resolved": "string",
  345. "references": {}
  346. },
  347. "required": false,
  348. "optional": true,
  349. "docs": {
  350. "tags": [{
  351. "name": "default",
  352. "text": "\"Loading\""
  353. }],
  354. "text": "string to override English loading text"
  355. },
  356. "attribute": "intl-loading",
  357. "reflect": false,
  358. "defaultValue": "TEXT.loading"
  359. },
  360. "loading": {
  361. "type": "boolean",
  362. "mutable": false,
  363. "complexType": {
  364. "original": "boolean",
  365. "resolved": "boolean",
  366. "references": {}
  367. },
  368. "required": false,
  369. "optional": false,
  370. "docs": {
  371. "tags": [],
  372. "text": "optionally add a calcite-loader component to the button, disabling interaction."
  373. },
  374. "attribute": "loading",
  375. "reflect": true,
  376. "defaultValue": "false"
  377. },
  378. "name": {
  379. "type": "string",
  380. "mutable": false,
  381. "complexType": {
  382. "original": "string",
  383. "resolved": "string",
  384. "references": {}
  385. },
  386. "required": false,
  387. "optional": true,
  388. "docs": {
  389. "tags": [],
  390. "text": "The name attribute to apply to the button"
  391. },
  392. "attribute": "name",
  393. "reflect": false
  394. },
  395. "rel": {
  396. "type": "string",
  397. "mutable": false,
  398. "complexType": {
  399. "original": "string",
  400. "resolved": "string",
  401. "references": {}
  402. },
  403. "required": false,
  404. "optional": true,
  405. "docs": {
  406. "tags": [],
  407. "text": "The rel attribute to apply to the hyperlink"
  408. },
  409. "attribute": "rel",
  410. "reflect": false
  411. },
  412. "form": {
  413. "type": "string",
  414. "mutable": false,
  415. "complexType": {
  416. "original": "string",
  417. "resolved": "string",
  418. "references": {}
  419. },
  420. "required": false,
  421. "optional": true,
  422. "docs": {
  423. "tags": [{
  424. "name": "deprecated",
  425. "text": "\u2013 this property is no longer needed if placed inside a form."
  426. }],
  427. "text": "The form ID to associate with the component"
  428. },
  429. "attribute": "form",
  430. "reflect": false
  431. },
  432. "round": {
  433. "type": "boolean",
  434. "mutable": false,
  435. "complexType": {
  436. "original": "boolean",
  437. "resolved": "boolean",
  438. "references": {}
  439. },
  440. "required": false,
  441. "optional": false,
  442. "docs": {
  443. "tags": [],
  444. "text": "optionally add a round style to the button"
  445. },
  446. "attribute": "round",
  447. "reflect": true,
  448. "defaultValue": "false"
  449. },
  450. "scale": {
  451. "type": "string",
  452. "mutable": false,
  453. "complexType": {
  454. "original": "Scale",
  455. "resolved": "\"l\" | \"m\" | \"s\"",
  456. "references": {
  457. "Scale": {
  458. "location": "import",
  459. "path": "../interfaces"
  460. }
  461. }
  462. },
  463. "required": false,
  464. "optional": false,
  465. "docs": {
  466. "tags": [],
  467. "text": "specify the scale of the button, defaults to m"
  468. },
  469. "attribute": "scale",
  470. "reflect": true,
  471. "defaultValue": "\"m\""
  472. },
  473. "splitChild": {
  474. "type": "any",
  475. "mutable": false,
  476. "complexType": {
  477. "original": "\"primary\" | \"secondary\" | false",
  478. "resolved": "\"primary\" | \"secondary\" | boolean",
  479. "references": {}
  480. },
  481. "required": false,
  482. "optional": true,
  483. "docs": {
  484. "tags": [],
  485. "text": "is the button a child of a calcite-split-button"
  486. },
  487. "attribute": "split-child",
  488. "reflect": true,
  489. "defaultValue": "false"
  490. },
  491. "target": {
  492. "type": "string",
  493. "mutable": false,
  494. "complexType": {
  495. "original": "string",
  496. "resolved": "string",
  497. "references": {}
  498. },
  499. "required": false,
  500. "optional": true,
  501. "docs": {
  502. "tags": [],
  503. "text": "The target attribute to apply to the hyperlink"
  504. },
  505. "attribute": "target",
  506. "reflect": false
  507. },
  508. "type": {
  509. "type": "string",
  510. "mutable": true,
  511. "complexType": {
  512. "original": "string",
  513. "resolved": "string",
  514. "references": {}
  515. },
  516. "required": false,
  517. "optional": false,
  518. "docs": {
  519. "tags": [],
  520. "text": "The type attribute to apply to the button"
  521. },
  522. "attribute": "type",
  523. "reflect": false,
  524. "defaultValue": "\"button\""
  525. },
  526. "width": {
  527. "type": "string",
  528. "mutable": false,
  529. "complexType": {
  530. "original": "Width",
  531. "resolved": "\"auto\" | \"full\" | \"half\"",
  532. "references": {
  533. "Width": {
  534. "location": "import",
  535. "path": "../interfaces"
  536. }
  537. }
  538. },
  539. "required": false,
  540. "optional": false,
  541. "docs": {
  542. "tags": [],
  543. "text": "specify the width of the button, defaults to auto"
  544. },
  545. "attribute": "width",
  546. "reflect": true,
  547. "defaultValue": "\"auto\""
  548. }
  549. }; }
  550. static get states() { return {
  551. "hasContent": {},
  552. "hasLoader": {}
  553. }; }
  554. static get methods() { return {
  555. "setFocus": {
  556. "complexType": {
  557. "signature": "() => Promise<void>",
  558. "parameters": [],
  559. "references": {
  560. "Promise": {
  561. "location": "global"
  562. }
  563. },
  564. "return": "Promise<void>"
  565. },
  566. "docs": {
  567. "text": "Sets focus on the component.",
  568. "tags": []
  569. }
  570. }
  571. }; }
  572. static get elementRef() { return "el"; }
  573. static get watchers() { return [{
  574. "propName": "loading",
  575. "methodName": "loadingChanged"
  576. }]; }
  577. }