button.js 19 KB

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