@hashicorp/design-system-components 2.13.0 → 2.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @hashicorp/design-system-components
2
2
 
3
+ ## 2.14.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1709](https://github.com/hashicorp/design-system/pull/1709) [`294dddfda`](https://github.com/hashicorp/design-system/commit/294dddfda8d8e174fd5dd7de5d6af01dd8405775) Thanks [@didoo](https://github.com/didoo)! - `Tabs` - Fixed issue with `@isSelected` dynamically changed within `#each` loops
8
+
9
+ ## 2.14.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [#1700](https://github.com/hashicorp/design-system/pull/1700) [`33d760fb8`](https://github.com/hashicorp/design-system/commit/33d760fb88d3945be8b50302a9bb7dce3ae221fe) Thanks [@didoo](https://github.com/didoo)! - `Pagination::Compact` - Added option to show "SizeSelector" element
14
+
15
+ - [#1688](https://github.com/hashicorp/design-system/pull/1688) [`c842b6eb7`](https://github.com/hashicorp/design-system/commit/c842b6eb731d82146b0e1ad8b9f55930b58aba18) Thanks [@didoo](https://github.com/didoo)! - `Tabs` - Refactored logic for `Tabs` component + `Tab/Panel` sub-components to support more complex use cases:
16
+
17
+ - introduced `@selectedTabIndex` argument to control the "selected" tab from the consuming application, e.g. via query params (effort spearheaded by @MiniHeyd)
18
+ - fixed issue with nested tabs not initializing the "selected" indicator correctly
19
+ - fixed issue with dynamic tab content not updating the "selected" indicator correctly
20
+
3
21
  ## 2.13.0
4
22
 
5
23
  ### Minor Changes
@@ -27,4 +27,13 @@
27
27
  @disabled={{@isDisabledNext}}
28
28
  />
29
29
  </nav>
30
+
31
+ {{#if this.showSizeSelector}}
32
+ <Hds::Pagination::SizeSelector
33
+ @pageSizes={{this.pageSizes}}
34
+ @label={{@sizeSelectorLabel}}
35
+ @selectedSize={{this.currentPageSize}}
36
+ @onChange={{this.onPageSizeChange}}
37
+ />
38
+ {{/if}}
30
39
  </div>
@@ -8,9 +8,15 @@ import { action } from '@ember/object';
8
8
  import { assert } from '@ember/debug';
9
9
  import { inject as service } from '@ember/service';
10
10
 
11
+ // for context about the decision to use these values, see:
12
+ // https://hashicorp.slack.com/archives/C03A0N1QK8S/p1673546329082759
13
+ export const DEFAULT_PAGE_SIZES = [10, 30, 50];
14
+
11
15
  export default class HdsPaginationCompactIndexComponent extends Component {
12
16
  @service router;
13
17
 
18
+ showSizeSelector = this.args.showSizeSelector ?? false; // if the "size selector" block is visible
19
+
14
20
  constructor() {
15
21
  super(...arguments);
16
22
 
@@ -56,6 +62,23 @@ export default class HdsPaginationCompactIndexComponent extends Component {
56
62
  return this.args.ariaLabel ?? 'Pagination';
57
63
  }
58
64
 
65
+ /**
66
+ * @param pageSizes
67
+ * @type {array of numbers}
68
+ * @description Set the page sizes users can select from.
69
+ * @default [10, 30, 50]
70
+ */
71
+ get pageSizes() {
72
+ let { pageSizes = DEFAULT_PAGE_SIZES } = this.args;
73
+
74
+ assert(
75
+ `pageSizes argument must be an array. Received: ${pageSizes}`,
76
+ Array.isArray(pageSizes) === true
77
+ );
78
+
79
+ return pageSizes;
80
+ }
81
+
59
82
  get routeQueryParams() {
60
83
  return this.router.currentRoute?.queryParams || {};
61
84
  }
@@ -3,7 +3,14 @@
3
3
  SPDX-License-Identifier: MPL-2.0
4
4
  }}
5
5
  {{! template-lint-disable no-invalid-role }}
6
- <div class="hds-tabs" {{did-insert this.didInsert}} ...attributes>
6
+ <div
7
+ class="hds-tabs"
8
+ {{did-insert this.didInsert}}
9
+ {{did-update this.didUpdateSelectedTabIndex this.selectedTabIndex}}
10
+ {{did-update this.didUpdateSelectedTabId this.selectedTabId}}
11
+ {{did-update this.didUpdateParentVisibility @isParentVisible}}
12
+ ...attributes
13
+ >
7
14
  <div class="hds-tabs__tablist-wrapper">
8
15
  <ul class="hds-tabs__tablist" role="tablist">
9
16
  {{yield
@@ -11,9 +18,9 @@
11
18
  Tab=(component
12
19
  "hds/tabs/tab"
13
20
  didInsertNode=this.didInsertTab
21
+ didUpdateNode=this.didUpdateTab
14
22
  willDestroyNode=this.willDestroyTab
15
23
  tabIds=this.tabIds
16
- panelIds=this.panelIds
17
24
  selectedTabIndex=this.selectedTabIndex
18
25
  onClick=this.onClick
19
26
  onKeyUp=this.onKeyUp
@@ -7,45 +7,97 @@ import Component from '@glimmer/component';
7
7
  import { tracked } from '@glimmer/tracking';
8
8
  import { action } from '@ember/object';
9
9
  import { assert } from '@ember/debug';
10
- import { schedule } from '@ember/runloop';
10
+ import { next, schedule } from '@ember/runloop';
11
11
 
12
12
  export default class HdsTabsIndexComponent extends Component {
13
13
  @tracked tabNodes = [];
14
14
  @tracked tabIds = [];
15
15
  @tracked panelNodes = [];
16
16
  @tracked panelIds = [];
17
- @tracked selectedTabIndex;
17
+ @tracked _selectedTabIndex = this.args.selectedTabIndex ?? 0;
18
+ @tracked selectedTabId;
19
+ @tracked isControlled;
20
+
21
+ constructor() {
22
+ super(...arguments);
23
+
24
+ // this is to determine if the "selected" tab logic is controlled in the consumers' code or is maintained as an internal state
25
+ this.isControlled = this.args.selectedTabIndex !== undefined;
26
+ }
27
+
28
+ get selectedTabIndex() {
29
+ if (this.isControlled) {
30
+ return this.args.selectedTabIndex;
31
+ } else {
32
+ return this._selectedTabIndex;
33
+ }
34
+ }
35
+
36
+ set selectedTabIndex(value) {
37
+ if (this.isControlled) {
38
+ // noop
39
+ } else {
40
+ this._selectedTabIndex = value;
41
+ }
42
+ }
18
43
 
19
44
  @action
20
45
  didInsert() {
21
- // default starting tab index
22
- let initialTabIndex = 0;
23
- let selectedCount = 0;
24
-
25
- this.tabNodes.forEach((tabElement, index) => {
26
- if (tabElement.hasAttribute('data-is-selected')) {
27
- initialTabIndex = index;
28
- selectedCount++;
29
- }
46
+ assert(
47
+ 'The number of Tabs must be equal to the number of Panels',
48
+ this.tabNodes.length === this.panelNodes.length
49
+ );
50
+
51
+ if (this.selectedTabId) {
52
+ this.selectedTabIndex = this.tabIds.indexOf(this.selectedTabId);
53
+ }
54
+
55
+ schedule('afterRender', () => {
56
+ this.setTabIndicator();
30
57
  });
31
- this.selectedTabIndex = initialTabIndex;
58
+ }
32
59
 
60
+ @action
61
+ didUpdateSelectedTabIndex() {
33
62
  schedule('afterRender', () => {
34
- this.setTabIndicator(initialTabIndex);
63
+ this.setTabIndicator();
35
64
  });
65
+ }
36
66
 
37
- assert('Only one tab may use isSelected argument', selectedCount <= 1);
67
+ @action
68
+ didUpdateSelectedTabId() {
69
+ // if the selected tab is set dynamically (eg. in a `each` loop)
70
+ // the `Tab` nodes will be re-inserted/rendered, which means the `this.selectedTabId` variable changes
71
+ // but the parent `Tabs` component has already been rendered/inserted but doesn't re-render
72
+ // so the value of the `selectedTabIndex` is not updated, unless we trigger a recalculation
73
+ // using the `did-update` modifier that checks for changes in the `this.selectedTabId` variable
74
+ if (this.selectedTabId) {
75
+ this.selectedTabIndex = this.tabIds.indexOf(this.selectedTabId);
76
+ }
77
+ }
38
78
 
39
- assert(
40
- 'The number of Tabs must be equal to the number of Panels',
41
- this.tabNodes.length === this.panelNodes.length
42
- );
79
+ @action
80
+ didUpdateParentVisibility() {
81
+ schedule('afterRender', () => {
82
+ this.setTabIndicator();
83
+ });
43
84
  }
44
85
 
45
86
  @action
46
- didInsertTab(element) {
87
+ didInsertTab(element, isSelected) {
47
88
  this.tabNodes = [...this.tabNodes, element];
48
89
  this.tabIds = [...this.tabIds, element.id];
90
+ if (isSelected) {
91
+ this.selectedTabId = element.id;
92
+ }
93
+ }
94
+
95
+ @action
96
+ didUpdateTab(tabIndex, isSelected) {
97
+ if (isSelected) {
98
+ this.selectedTabIndex = tabIndex;
99
+ }
100
+ this.setTabIndicator();
49
101
  }
50
102
 
51
103
  @action
@@ -55,7 +107,7 @@ export default class HdsTabsIndexComponent extends Component {
55
107
  }
56
108
 
57
109
  @action
58
- didInsertPanel(panelId, element) {
110
+ didInsertPanel(element, panelId) {
59
111
  this.panelNodes = [...this.panelNodes, element];
60
112
  this.panelIds = [...this.panelIds, panelId];
61
113
  }
@@ -67,40 +119,39 @@ export default class HdsTabsIndexComponent extends Component {
67
119
  }
68
120
 
69
121
  @action
70
- onClick(tabIndex, event) {
122
+ onClick(event, tabIndex) {
71
123
  this.selectedTabIndex = tabIndex;
72
- this.setTabIndicator(tabIndex);
73
-
74
- // Scroll Tab into view if it's out of view
75
- this.tabNodes[tabIndex].parentNode.scrollIntoView({
76
- behavior: 'smooth',
77
- block: 'nearest',
78
- inline: 'nearest',
79
- });
124
+ this.setTabIndicator();
80
125
 
81
126
  // invoke the callback function if it's provided as argument
82
127
  if (typeof this.args.onClickTab === 'function') {
83
- this.args.onClickTab(event);
128
+ this.args.onClickTab(event, tabIndex);
84
129
  }
85
130
  }
86
131
 
87
132
  @action
88
- onKeyUp(tabIndex, e) {
133
+ onKeyUp(tabIndex, event) {
89
134
  const leftArrow = 37;
90
135
  const rightArrow = 39;
91
136
  const enterKey = 13;
92
137
  const spaceKey = 32;
93
138
 
94
- if (e.keyCode === rightArrow) {
139
+ if (event.keyCode === rightArrow) {
95
140
  const nextTabIndex = (tabIndex + 1) % this.tabIds.length;
96
- this.focusTab(nextTabIndex, e);
97
- } else if (e.keyCode === leftArrow) {
141
+ this.focusTab(nextTabIndex, event);
142
+ } else if (event.keyCode === leftArrow) {
98
143
  const prevTabIndex =
99
144
  (tabIndex + this.tabIds.length - 1) % this.tabIds.length;
100
- this.focusTab(prevTabIndex, e);
101
- } else if (e.keyCode === enterKey || e.keyCode === spaceKey) {
145
+ this.focusTab(prevTabIndex, event);
146
+ } else if (event.keyCode === enterKey || event.keyCode === spaceKey) {
102
147
  this.selectedTabIndex = tabIndex;
103
148
  }
149
+ // scroll selected tab into view (it may be out of view when activated using a keyboard with `prev/next`)
150
+ this.tabNodes[this.selectedTabIndex].parentNode.scrollIntoView({
151
+ behavior: 'smooth',
152
+ block: 'nearest',
153
+ inline: 'nearest',
154
+ });
104
155
  }
105
156
 
106
157
  // Focus tab for keyboard & mouse navigation:
@@ -114,15 +165,37 @@ export default class HdsTabsIndexComponent extends Component {
114
165
  this.panelNodes[tabIndex].focus();
115
166
  }
116
167
 
117
- setTabIndicator(tabIndex) {
118
- const tabElem = this.tabNodes[tabIndex];
119
- const tabsParentElem = tabElem.closest('.hds-tabs');
120
-
121
- const tabLeftPos = tabElem.parentNode.offsetLeft;
122
- const tabWidth = tabElem.parentNode.offsetWidth;
123
-
124
- // Set CSS custom properties for indicator
125
- tabsParentElem.style.setProperty('--indicator-left-pos', tabLeftPos + 'px');
126
- tabsParentElem.style.setProperty('--indicator-width', tabWidth + 'px');
168
+ setTabIndicator() {
169
+ next(() => {
170
+ const tabElem = this.tabNodes[this.selectedTabIndex];
171
+
172
+ if (tabElem) {
173
+ const tabsParentElem = tabElem.closest('.hds-tabs__tablist');
174
+
175
+ // this condition is `null` if any of the parents has `display: none`
176
+ if (tabElem.parentNode.offsetParent) {
177
+ const tabLeftPos = tabElem.parentNode.offsetLeft;
178
+ const tabWidth = tabElem.parentNode.offsetWidth;
179
+
180
+ // Set CSS custom properties for indicator
181
+ tabsParentElem.style.setProperty(
182
+ '--indicator-left-pos',
183
+ tabLeftPos + 'px'
184
+ );
185
+ tabsParentElem.style.setProperty(
186
+ '--indicator-width',
187
+ tabWidth + 'px'
188
+ );
189
+ }
190
+ } else {
191
+ assert(
192
+ `"Hds::Tabs" has tried to set the indicator for an element that doesn't exist (the value ${
193
+ this.selectedTabIndex
194
+ } of \`this.selectedTabIndex\` is out of bound for the array \`this.tabNodes\`, whose index range is [0-${
195
+ this.tabNodes.length - 1
196
+ }])`
197
+ );
198
+ }
199
+ });
127
200
  }
128
201
  }
@@ -6,11 +6,11 @@
6
6
  class="hds-tabs__panel"
7
7
  ...attributes
8
8
  role="tabpanel"
9
- aria-labelledby={{this.tabId}}
10
9
  id={{this.panelId}}
11
- hidden={{not this.isSelected}}
10
+ hidden={{not this.isVisible}}
11
+ aria-labelledby={{this.coupledTabId}}
12
12
  {{did-insert this.didInsertNode}}
13
13
  {{will-destroy this.willDestroyNode}}
14
14
  >
15
- {{yield}}
15
+ {{yield (hash isVisible=this.isVisible)}}
16
16
  </section>
@@ -10,9 +10,8 @@ import { action } from '@ember/object';
10
10
 
11
11
  export default class HdsTabsIndexComponent extends Component {
12
12
  /**
13
- * Generates a unique ID for the Panel
14
- *
15
- * @param panelId
13
+ * Generate a unique ID for the Panel
14
+ * @return {string}
16
15
  */
17
16
  panelId = 'panel-' + guidFor(this);
18
17
 
@@ -23,32 +22,40 @@ export default class HdsTabsIndexComponent extends Component {
23
22
  : undefined;
24
23
  }
25
24
 
26
- get tabId() {
25
+ /**
26
+ * Check the condition if the panel is visible (because the coupled/associated tab is selected) or not
27
+ * @returns {boolean}
28
+ */
29
+ get isVisible() {
30
+ return this.nodeIndex === this.args.selectedTabIndex;
31
+ }
32
+
33
+ /**
34
+ * Get the ID of the tab coupled/associated with the panel (it's used by the `aria-labelledby` attribute)
35
+ * @returns string}
36
+ */
37
+ get coupledTabId() {
27
38
  return this.nodeIndex !== undefined
28
39
  ? this.args.tabIds[this.nodeIndex]
29
40
  : undefined;
30
41
  }
31
42
 
32
- get isSelected() {
33
- return this.nodeIndex === this.args.selectedTabIndex;
34
- }
35
-
36
43
  @action
37
44
  didInsertNode(element) {
38
45
  let { didInsertNode } = this.args;
39
46
 
40
47
  if (typeof didInsertNode === 'function') {
41
48
  this.elementId = element.id;
42
- didInsertNode(this.elementId, ...arguments);
49
+ didInsertNode(element, this.elementId);
43
50
  }
44
51
  }
45
52
 
46
53
  @action
47
- willDestroyNode() {
54
+ willDestroyNode(element) {
48
55
  let { willDestroyNode } = this.args;
49
56
 
50
57
  if (typeof willDestroyNode === 'function') {
51
- willDestroyNode(...arguments);
58
+ willDestroyNode(element);
52
59
  }
53
60
  }
54
61
  }
@@ -11,8 +11,8 @@
11
11
  id={{this.tabId}}
12
12
  aria-selected={{if this.isSelected "true" "false"}}
13
13
  tabindex={{unless this.isSelected "-1"}}
14
- data-is-selected={{this.isInitialTab}}
15
- {{did-insert this.didInsertNode}}
14
+ {{did-insert this.didInsertNode @isSelected}}
15
+ {{did-update this.didUpdateNode @count @isSelected}}
16
16
  {{will-destroy this.willDestroyNode}}
17
17
  {{on "click" this.onClick}}
18
18
  {{on "keyup" this.onKeyUp}}
@@ -10,9 +10,8 @@ import { action } from '@ember/object';
10
10
 
11
11
  export default class HdsTabsIndexComponent extends Component {
12
12
  /**
13
- * Generates a unique ID for the Tab
14
- *
15
- * @param tabId
13
+ * Generate a unique ID for the Tab
14
+ * @return {string}
16
15
  */
17
16
  tabId = 'tab-' + guidFor(this);
18
17
 
@@ -21,17 +20,10 @@ export default class HdsTabsIndexComponent extends Component {
21
20
  return this.args.tabIds ? this.args.tabIds.indexOf(this.tabId) : undefined;
22
21
  }
23
22
 
24
- get panelId() {
25
- return this.nodeIndex !== undefined
26
- ? this.args.panelIds[this.nodeIndex]
27
- : undefined;
28
- }
29
-
30
23
  /**
31
- * @param isSelected
32
- * @type {boolean}
24
+ * Determine if the tab is the selected tab
25
+ * @return {boolean}
33
26
  * @default false (1st tab is selected by default)
34
- * @description Determines if the tab is the selected tab
35
27
  */
36
28
  get isSelected() {
37
29
  return (
@@ -40,46 +32,52 @@ export default class HdsTabsIndexComponent extends Component {
40
32
  );
41
33
  }
42
34
 
43
- get isInitialTab() {
44
- let { isSelected } = this.args;
45
- return isSelected;
46
- }
47
-
48
35
  @action
49
- didInsertNode() {
36
+ didInsertNode(element, positional) {
50
37
  let { didInsertNode } = this.args;
51
38
 
39
+ const isSelected = positional[0];
40
+
52
41
  if (typeof didInsertNode === 'function') {
53
- didInsertNode(...arguments);
42
+ didInsertNode(element, isSelected);
43
+ }
44
+ }
45
+
46
+ @action
47
+ didUpdateNode() {
48
+ let { didUpdateNode } = this.args;
49
+
50
+ if (typeof didUpdateNode === 'function') {
51
+ didUpdateNode(this.nodeIndex, this.args.isSelected);
54
52
  }
55
53
  }
56
54
 
57
55
  @action
58
- willDestroyNode() {
56
+ willDestroyNode(element) {
59
57
  let { willDestroyNode } = this.args;
60
58
 
61
59
  if (typeof willDestroyNode === 'function') {
62
- willDestroyNode(...arguments);
60
+ willDestroyNode(element);
63
61
  }
64
62
  }
65
63
 
66
64
  @action
67
- onClick() {
65
+ onClick(event) {
68
66
  let { onClick } = this.args;
69
67
 
70
68
  if (typeof onClick === 'function') {
71
- onClick(this.nodeIndex, ...arguments);
69
+ onClick(event, this.nodeIndex);
72
70
  } else {
73
71
  return false;
74
72
  }
75
73
  }
76
74
 
77
75
  @action
78
- onKeyUp() {
76
+ onKeyUp(event) {
79
77
  let { onKeyUp } = this.args;
80
78
 
81
79
  if (typeof onKeyUp === 'function') {
82
- onKeyUp(this.nodeIndex, ...arguments);
80
+ onKeyUp(this.nodeIndex, event);
83
81
  } else {
84
82
  return false;
85
83
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hashicorp/design-system-components",
3
- "version": "2.13.0",
3
+ "version": "2.14.1",
4
4
  "description": "Helios Design System Components",
5
5
  "keywords": [
6
6
  "hashicorp",