@hashicorp/design-system-components 2.13.0 → 2.14.0

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,17 @@
1
1
  # @hashicorp/design-system-components
2
2
 
3
+ ## 2.14.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#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
8
+
9
+ - [#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:
10
+
11
+ - introduced `@selectedTabIndex` argument to control the "selected" tab from the consuming application, e.g. via query params (effort spearheaded by @MiniHeyd)
12
+ - fixed issue with nested tabs not initializing the "selected" indicator correctly
13
+ - fixed issue with dynamic tab content not updating the "selected" indicator correctly
14
+
3
15
  ## 2.13.0
4
16
 
5
17
  ### 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,12 @@
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.updateTabIndicator this.selectedTabIndex @isParentVisible}}
10
+ ...attributes
11
+ >
7
12
  <div class="hds-tabs__tablist-wrapper">
8
13
  <ul class="hds-tabs__tablist" role="tablist">
9
14
  {{yield
@@ -11,9 +16,9 @@
11
16
  Tab=(component
12
17
  "hds/tabs/tab"
13
18
  didInsertNode=this.didInsertTab
19
+ didUpdateNode=this.didUpdateTab
14
20
  willDestroyNode=this.willDestroyTab
15
21
  tabIds=this.tabIds
16
- panelIds=this.panelIds
17
22
  selectedTabIndex=this.selectedTabIndex
18
23
  onClick=this.onClick
19
24
  onKeyUp=this.onKeyUp
@@ -7,45 +7,74 @@ 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;
18
20
 
19
- @action
20
- 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
- }
30
- });
31
- this.selectedTabIndex = initialTabIndex;
21
+ constructor() {
22
+ super(...arguments);
32
23
 
33
- schedule('afterRender', () => {
34
- this.setTabIndicator(initialTabIndex);
35
- });
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
+ }
36
35
 
37
- assert('Only one tab may use isSelected argument', selectedCount <= 1);
36
+ set selectedTabIndex(value) {
37
+ if (this.isControlled) {
38
+ // noop
39
+ } else {
40
+ this._selectedTabIndex = value;
41
+ }
42
+ }
38
43
 
44
+ @action
45
+ didInsert() {
39
46
  assert(
40
47
  'The number of Tabs must be equal to the number of Panels',
41
48
  this.tabNodes.length === this.panelNodes.length
42
49
  );
50
+
51
+ if (this.selectedTabId) {
52
+ this.selectedTabIndex = this.tabIds.indexOf(this.selectedTabId);
53
+ }
54
+
55
+ schedule('afterRender', () => {
56
+ this.setTabIndicator();
57
+ });
43
58
  }
44
59
 
45
60
  @action
46
- didInsertTab(element) {
61
+ didInsertTab(element, isSelected) {
47
62
  this.tabNodes = [...this.tabNodes, element];
48
63
  this.tabIds = [...this.tabIds, element.id];
64
+ if (isSelected) {
65
+ if (this.selectedTabId) {
66
+ assert('Only one tab may use isSelected argument');
67
+ }
68
+ this.selectedTabId = element.id;
69
+ }
70
+ }
71
+
72
+ @action
73
+ didUpdateTab(tabIndex, isSelected) {
74
+ if (isSelected) {
75
+ this.selectedTabIndex = tabIndex;
76
+ }
77
+ this.setTabIndicator();
49
78
  }
50
79
 
51
80
  @action
@@ -55,7 +84,7 @@ export default class HdsTabsIndexComponent extends Component {
55
84
  }
56
85
 
57
86
  @action
58
- didInsertPanel(panelId, element) {
87
+ didInsertPanel(element, panelId) {
59
88
  this.panelNodes = [...this.panelNodes, element];
60
89
  this.panelIds = [...this.panelIds, panelId];
61
90
  }
@@ -67,40 +96,39 @@ export default class HdsTabsIndexComponent extends Component {
67
96
  }
68
97
 
69
98
  @action
70
- onClick(tabIndex, event) {
99
+ onClick(event, tabIndex) {
71
100
  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
- });
101
+ this.setTabIndicator();
80
102
 
81
103
  // invoke the callback function if it's provided as argument
82
104
  if (typeof this.args.onClickTab === 'function') {
83
- this.args.onClickTab(event);
105
+ this.args.onClickTab(event, tabIndex);
84
106
  }
85
107
  }
86
108
 
87
109
  @action
88
- onKeyUp(tabIndex, e) {
110
+ onKeyUp(tabIndex, event) {
89
111
  const leftArrow = 37;
90
112
  const rightArrow = 39;
91
113
  const enterKey = 13;
92
114
  const spaceKey = 32;
93
115
 
94
- if (e.keyCode === rightArrow) {
116
+ if (event.keyCode === rightArrow) {
95
117
  const nextTabIndex = (tabIndex + 1) % this.tabIds.length;
96
- this.focusTab(nextTabIndex, e);
97
- } else if (e.keyCode === leftArrow) {
118
+ this.focusTab(nextTabIndex, event);
119
+ } else if (event.keyCode === leftArrow) {
98
120
  const prevTabIndex =
99
121
  (tabIndex + this.tabIds.length - 1) % this.tabIds.length;
100
- this.focusTab(prevTabIndex, e);
101
- } else if (e.keyCode === enterKey || e.keyCode === spaceKey) {
122
+ this.focusTab(prevTabIndex, event);
123
+ } else if (event.keyCode === enterKey || event.keyCode === spaceKey) {
102
124
  this.selectedTabIndex = tabIndex;
103
125
  }
126
+ // scroll selected tab into view (it may be out of view when activated using a keyboard with `prev/next`)
127
+ this.tabNodes[this.selectedTabIndex].parentNode.scrollIntoView({
128
+ behavior: 'smooth',
129
+ block: 'nearest',
130
+ inline: 'nearest',
131
+ });
104
132
  }
105
133
 
106
134
  // Focus tab for keyboard & mouse navigation:
@@ -114,15 +142,42 @@ export default class HdsTabsIndexComponent extends Component {
114
142
  this.panelNodes[tabIndex].focus();
115
143
  }
116
144
 
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;
145
+ setTabIndicator() {
146
+ next(() => {
147
+ const tabElem = this.tabNodes[this.selectedTabIndex];
148
+
149
+ if (tabElem) {
150
+ const tabsParentElem = tabElem.closest('.hds-tabs__tablist');
151
+
152
+ // this condition is `null` if any of the parents has `display: none`
153
+ if (tabElem.parentNode.offsetParent) {
154
+ const tabLeftPos = tabElem.parentNode.offsetLeft;
155
+ const tabWidth = tabElem.parentNode.offsetWidth;
156
+
157
+ // Set CSS custom properties for indicator
158
+ tabsParentElem.style.setProperty(
159
+ '--indicator-left-pos',
160
+ tabLeftPos + 'px'
161
+ );
162
+ tabsParentElem.style.setProperty(
163
+ '--indicator-width',
164
+ tabWidth + 'px'
165
+ );
166
+ }
167
+ } else {
168
+ assert(
169
+ `"Hds::Tabs" has tried to set the indicator for an element that doesn't exist (the value ${
170
+ this.selectedTabIndex
171
+ } of \`this.selectedTabIndex\` is out of bound for the array \`this.tabNodes\`, whose index range is [0-${
172
+ this.tabNodes.length - 1
173
+ }])`
174
+ );
175
+ }
176
+ });
177
+ }
123
178
 
124
- // Set CSS custom properties for indicator
125
- tabsParentElem.style.setProperty('--indicator-left-pos', tabLeftPos + 'px');
126
- tabsParentElem.style.setProperty('--indicator-width', tabWidth + 'px');
179
+ @action
180
+ updateTabIndicator() {
181
+ this.setTabIndicator();
127
182
  }
128
183
  }
@@ -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.0",
4
4
  "description": "Helios Design System Components",
5
5
  "keywords": [
6
6
  "hashicorp",