tab-nav.js 15 KB

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