tab-nav.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  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 { h, Host } from "@stencil/core";
  7. import { getElementDir, filterDirectChildren } from "../../utils/dom";
  8. import { createObserver } from "../../utils/observers";
  9. /**
  10. * @slot - A slot for adding `calcite-tab-title`s.
  11. */
  12. export class TabNav {
  13. constructor() {
  14. /**
  15. * @internal
  16. */
  17. this.scale = "m";
  18. /**
  19. * @internal
  20. */
  21. this.layout = "inline";
  22. /**
  23. * @internal
  24. */
  25. this.position = "bottom";
  26. /**
  27. * @internal
  28. */
  29. this.bordered = false;
  30. this.animationActiveDuration = 0.3;
  31. this.resizeObserver = createObserver("resize", () => {
  32. // remove active indicator transition duration during resize to prevent wobble
  33. this.activeIndicatorEl.style.transitionDuration = "0s";
  34. this.updateActiveWidth();
  35. this.updateOffsetPosition();
  36. });
  37. //--------------------------------------------------------------------------
  38. //
  39. // Private Methods
  40. //
  41. //--------------------------------------------------------------------------
  42. this.handleContainerScroll = () => {
  43. // remove active indicator transition duration while container is scrolling to prevent wobble
  44. this.activeIndicatorEl.style.transitionDuration = "0s";
  45. this.updateOffsetPosition();
  46. };
  47. }
  48. async selectedTabChanged() {
  49. if (localStorage &&
  50. this.storageId &&
  51. this.selectedTab !== undefined &&
  52. this.selectedTab !== null) {
  53. localStorage.setItem(`calcite-tab-nav-${this.storageId}`, JSON.stringify(this.selectedTab));
  54. }
  55. this.calciteInternalTabChange.emit({
  56. tab: this.selectedTab
  57. });
  58. this.selectedTabEl = await this.getTabTitleById(this.selectedTab);
  59. }
  60. selectedTabElChanged() {
  61. this.updateOffsetPosition();
  62. this.updateActiveWidth();
  63. // reset the animation time on tab selection
  64. this.activeIndicatorEl.style.transitionDuration = `${this.animationActiveDuration}s`;
  65. }
  66. //--------------------------------------------------------------------------
  67. //
  68. // Lifecycle
  69. //
  70. //--------------------------------------------------------------------------
  71. connectedCallback() {
  72. var _a;
  73. this.parentTabsEl = this.el.closest("calcite-tabs");
  74. (_a = this.resizeObserver) === null || _a === void 0 ? void 0 : _a.observe(this.el);
  75. }
  76. disconnectedCallback() {
  77. var _a;
  78. (_a = this.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
  79. }
  80. componentWillLoad() {
  81. const storageKey = `calcite-tab-nav-${this.storageId}`;
  82. if (localStorage && this.storageId && localStorage.getItem(storageKey)) {
  83. const storedTab = JSON.parse(localStorage.getItem(storageKey));
  84. this.selectedTab = storedTab;
  85. }
  86. }
  87. componentWillRender() {
  88. const { parentTabsEl } = this;
  89. this.layout = parentTabsEl === null || parentTabsEl === void 0 ? void 0 : parentTabsEl.layout;
  90. this.position = parentTabsEl === null || parentTabsEl === void 0 ? void 0 : parentTabsEl.position;
  91. this.scale = parentTabsEl === null || parentTabsEl === void 0 ? void 0 : parentTabsEl.scale;
  92. this.bordered = parentTabsEl === null || parentTabsEl === void 0 ? void 0 : parentTabsEl.bordered;
  93. // fix issue with active tab-title not lining up with blue indicator
  94. if (this.selectedTabEl) {
  95. this.updateOffsetPosition();
  96. }
  97. }
  98. componentDidRender() {
  99. // if every tab title is active select the first tab.
  100. if (this.tabTitles.length &&
  101. this.tabTitles.every((title) => !title.active) &&
  102. !this.selectedTab) {
  103. this.tabTitles[0].getTabIdentifier().then((tab) => {
  104. this.calciteInternalTabChange.emit({
  105. tab
  106. });
  107. });
  108. }
  109. }
  110. render() {
  111. const dir = getElementDir(this.el);
  112. const width = `${this.indicatorWidth}px`;
  113. const offset = `${this.indicatorOffset}px`;
  114. const indicatorStyle = dir !== "rtl" ? { width, left: offset } : { width, right: offset };
  115. return (h(Host, { role: "tablist" }, h("div", { class: "tab-nav", onScroll: this.handleContainerScroll, ref: (el) => (this.tabNavEl = el) }, h("div", { class: "tab-nav-active-indicator-container", ref: (el) => (this.activeIndicatorContainerEl = el) }, h("div", { class: "tab-nav-active-indicator", ref: (el) => (this.activeIndicatorEl = el), style: indicatorStyle })), h("slot", null))));
  116. }
  117. //--------------------------------------------------------------------------
  118. //
  119. // Event Listeners
  120. //
  121. //--------------------------------------------------------------------------
  122. focusPreviousTabHandler(event) {
  123. const currentIndex = this.getIndexOfTabTitle(event.target, this.enabledTabTitles);
  124. const previousTab = this.enabledTabTitles[currentIndex - 1] ||
  125. this.enabledTabTitles[this.enabledTabTitles.length - 1];
  126. previousTab === null || previousTab === void 0 ? void 0 : previousTab.focus();
  127. event.stopPropagation();
  128. event.preventDefault();
  129. }
  130. focusNextTabHandler(event) {
  131. const currentIndex = this.getIndexOfTabTitle(event.target, this.enabledTabTitles);
  132. const nextTab = this.enabledTabTitles[currentIndex + 1] || this.enabledTabTitles[0];
  133. nextTab === null || nextTab === void 0 ? void 0 : nextTab.focus();
  134. event.stopPropagation();
  135. event.preventDefault();
  136. }
  137. internalActivateTabHandler(event) {
  138. this.selectedTab = event.detail.tab
  139. ? event.detail.tab
  140. : this.getIndexOfTabTitle(event.target);
  141. event.stopPropagation();
  142. event.preventDefault();
  143. }
  144. activateTabHandler(event) {
  145. this.calciteTabChange.emit({
  146. tab: this.selectedTab
  147. });
  148. event.stopPropagation();
  149. event.preventDefault();
  150. }
  151. /**
  152. * Check for active tabs on register and update selected
  153. *
  154. * @param event
  155. */
  156. updateTabTitles(event) {
  157. if (event.target.active) {
  158. this.selectedTab = event.detail;
  159. }
  160. }
  161. globalInternalTabChangeHandler(event) {
  162. if (this.syncId &&
  163. event.target !== this.el &&
  164. event.target.syncId === this.syncId &&
  165. this.selectedTab !== event.detail.tab) {
  166. this.selectedTab = event.detail.tab;
  167. }
  168. event.stopPropagation();
  169. }
  170. iconStartChangeHandler() {
  171. this.updateActiveWidth();
  172. }
  173. updateOffsetPosition() {
  174. var _a, _b, _c, _d, _e;
  175. const dir = getElementDir(this.el);
  176. const navWidth = (_a = this.activeIndicatorContainerEl) === null || _a === void 0 ? void 0 : _a.offsetWidth;
  177. const tabLeft = (_b = this.selectedTabEl) === null || _b === void 0 ? void 0 : _b.offsetLeft;
  178. const tabWidth = (_c = this.selectedTabEl) === null || _c === void 0 ? void 0 : _c.offsetWidth;
  179. const offsetRight = navWidth - (tabLeft + tabWidth);
  180. this.indicatorOffset =
  181. dir !== "rtl" ? tabLeft - ((_d = this.tabNavEl) === null || _d === void 0 ? void 0 : _d.scrollLeft) : offsetRight + ((_e = this.tabNavEl) === null || _e === void 0 ? void 0 : _e.scrollLeft);
  182. }
  183. updateActiveWidth() {
  184. var _a;
  185. this.indicatorWidth = (_a = this.selectedTabEl) === null || _a === void 0 ? void 0 : _a.offsetWidth;
  186. }
  187. getIndexOfTabTitle(el, tabTitles = this.tabTitles) {
  188. // In most cases, since these indexes correlate with tab contents, we want to consider all tab titles.
  189. // However, when doing relative index operations, it makes sense to pass in this.enabledTabTitles as the 2nd arg.
  190. return tabTitles.indexOf(el);
  191. }
  192. async getTabTitleById(id) {
  193. return Promise.all(this.tabTitles.map((el) => el.getTabIdentifier())).then((ids) => {
  194. return this.tabTitles[ids.indexOf(id)];
  195. });
  196. }
  197. get tabTitles() {
  198. return filterDirectChildren(this.el, "calcite-tab-title");
  199. }
  200. get enabledTabTitles() {
  201. return filterDirectChildren(this.el, "calcite-tab-title:not([disabled])");
  202. }
  203. static get is() { return "calcite-tab-nav"; }
  204. static get encapsulation() { return "shadow"; }
  205. static get originalStyleUrls() {
  206. return {
  207. "$": ["tab-nav.scss"]
  208. };
  209. }
  210. static get styleUrls() {
  211. return {
  212. "$": ["tab-nav.css"]
  213. };
  214. }
  215. static get properties() {
  216. return {
  217. "storageId": {
  218. "type": "string",
  219. "mutable": false,
  220. "complexType": {
  221. "original": "string",
  222. "resolved": "string",
  223. "references": {}
  224. },
  225. "required": false,
  226. "optional": false,
  227. "docs": {
  228. "tags": [],
  229. "text": "Specifies the name when saving selected `calcite-tab` data to `localStorage`."
  230. },
  231. "attribute": "storage-id",
  232. "reflect": true
  233. },
  234. "syncId": {
  235. "type": "string",
  236. "mutable": false,
  237. "complexType": {
  238. "original": "string",
  239. "resolved": "string",
  240. "references": {}
  241. },
  242. "required": false,
  243. "optional": false,
  244. "docs": {
  245. "tags": [],
  246. "text": "Specifies text to update multiple components to keep in sync if one changes."
  247. },
  248. "attribute": "sync-id",
  249. "reflect": true
  250. },
  251. "scale": {
  252. "type": "string",
  253. "mutable": true,
  254. "complexType": {
  255. "original": "Scale",
  256. "resolved": "\"l\" | \"m\" | \"s\"",
  257. "references": {
  258. "Scale": {
  259. "location": "import",
  260. "path": "../interfaces"
  261. }
  262. }
  263. },
  264. "required": false,
  265. "optional": false,
  266. "docs": {
  267. "tags": [{
  268. "name": "internal",
  269. "text": undefined
  270. }],
  271. "text": ""
  272. },
  273. "attribute": "scale",
  274. "reflect": true,
  275. "defaultValue": "\"m\""
  276. },
  277. "layout": {
  278. "type": "string",
  279. "mutable": true,
  280. "complexType": {
  281. "original": "TabLayout",
  282. "resolved": "\"center\" | \"inline\"",
  283. "references": {
  284. "TabLayout": {
  285. "location": "import",
  286. "path": "../tabs/interfaces"
  287. }
  288. }
  289. },
  290. "required": false,
  291. "optional": false,
  292. "docs": {
  293. "tags": [{
  294. "name": "internal",
  295. "text": undefined
  296. }],
  297. "text": ""
  298. },
  299. "attribute": "layout",
  300. "reflect": true,
  301. "defaultValue": "\"inline\""
  302. },
  303. "position": {
  304. "type": "string",
  305. "mutable": true,
  306. "complexType": {
  307. "original": "TabPosition",
  308. "resolved": "\"above\" | \"below\" | \"bottom\" | \"top\"",
  309. "references": {
  310. "TabPosition": {
  311. "location": "import",
  312. "path": "../tabs/interfaces"
  313. }
  314. }
  315. },
  316. "required": false,
  317. "optional": false,
  318. "docs": {
  319. "tags": [{
  320. "name": "internal",
  321. "text": undefined
  322. }],
  323. "text": ""
  324. },
  325. "attribute": "position",
  326. "reflect": true,
  327. "defaultValue": "\"bottom\""
  328. },
  329. "bordered": {
  330. "type": "boolean",
  331. "mutable": true,
  332. "complexType": {
  333. "original": "boolean",
  334. "resolved": "boolean",
  335. "references": {}
  336. },
  337. "required": false,
  338. "optional": false,
  339. "docs": {
  340. "tags": [{
  341. "name": "internal",
  342. "text": undefined
  343. }],
  344. "text": ""
  345. },
  346. "attribute": "bordered",
  347. "reflect": true,
  348. "defaultValue": "false"
  349. },
  350. "indicatorOffset": {
  351. "type": "number",
  352. "mutable": true,
  353. "complexType": {
  354. "original": "number",
  355. "resolved": "number",
  356. "references": {}
  357. },
  358. "required": false,
  359. "optional": false,
  360. "docs": {
  361. "tags": [{
  362. "name": "internal",
  363. "text": undefined
  364. }],
  365. "text": ""
  366. },
  367. "attribute": "indicator-offset",
  368. "reflect": false
  369. },
  370. "indicatorWidth": {
  371. "type": "number",
  372. "mutable": true,
  373. "complexType": {
  374. "original": "number",
  375. "resolved": "number",
  376. "references": {}
  377. },
  378. "required": false,
  379. "optional": false,
  380. "docs": {
  381. "tags": [{
  382. "name": "internal",
  383. "text": undefined
  384. }],
  385. "text": ""
  386. },
  387. "attribute": "indicator-width",
  388. "reflect": false
  389. }
  390. };
  391. }
  392. static get states() {
  393. return {
  394. "selectedTab": {},
  395. "selectedTabEl": {}
  396. };
  397. }
  398. static get events() {
  399. return [{
  400. "method": "calciteTabChange",
  401. "name": "calciteTabChange",
  402. "bubbles": true,
  403. "cancelable": false,
  404. "composed": true,
  405. "docs": {
  406. "tags": [{
  407. "name": "see",
  408. "text": "[TabChangeEventDetail](https://github.com/Esri/calcite-components/blob/master/src/components/tab/interfaces.ts#L1)"
  409. }],
  410. "text": "Emits when the selected `calcite-tab` changes."
  411. },
  412. "complexType": {
  413. "original": "TabChangeEventDetail",
  414. "resolved": "TabChangeEventDetail",
  415. "references": {
  416. "TabChangeEventDetail": {
  417. "location": "import",
  418. "path": "../tab/interfaces"
  419. }
  420. }
  421. }
  422. }, {
  423. "method": "calciteInternalTabChange",
  424. "name": "calciteInternalTabChange",
  425. "bubbles": true,
  426. "cancelable": false,
  427. "composed": true,
  428. "docs": {
  429. "tags": [{
  430. "name": "internal",
  431. "text": undefined
  432. }],
  433. "text": ""
  434. },
  435. "complexType": {
  436. "original": "TabChangeEventDetail",
  437. "resolved": "TabChangeEventDetail",
  438. "references": {
  439. "TabChangeEventDetail": {
  440. "location": "import",
  441. "path": "../tab/interfaces"
  442. }
  443. }
  444. }
  445. }];
  446. }
  447. static get elementRef() { return "el"; }
  448. static get watchers() {
  449. return [{
  450. "propName": "selectedTab",
  451. "methodName": "selectedTabChanged"
  452. }, {
  453. "propName": "selectedTabEl",
  454. "methodName": "selectedTabElChanged"
  455. }];
  456. }
  457. static get listeners() {
  458. return [{
  459. "name": "calciteInternalTabsFocusPrevious",
  460. "method": "focusPreviousTabHandler",
  461. "target": undefined,
  462. "capture": false,
  463. "passive": false
  464. }, {
  465. "name": "calciteInternalTabsFocusNext",
  466. "method": "focusNextTabHandler",
  467. "target": undefined,
  468. "capture": false,
  469. "passive": false
  470. }, {
  471. "name": "calciteInternalTabsActivate",
  472. "method": "internalActivateTabHandler",
  473. "target": undefined,
  474. "capture": false,
  475. "passive": false
  476. }, {
  477. "name": "calciteTabsActivate",
  478. "method": "activateTabHandler",
  479. "target": undefined,
  480. "capture": false,
  481. "passive": false
  482. }, {
  483. "name": "calciteInternalTabTitleRegister",
  484. "method": "updateTabTitles",
  485. "target": undefined,
  486. "capture": false,
  487. "passive": false
  488. }, {
  489. "name": "calciteInternalTabChange",
  490. "method": "globalInternalTabChangeHandler",
  491. "target": "body",
  492. "capture": false,
  493. "passive": false
  494. }, {
  495. "name": "calciteInternalTabIconChanged",
  496. "method": "iconStartChangeHandler",
  497. "target": undefined,
  498. "capture": false,
  499. "passive": false
  500. }];
  501. }
  502. }