Skip to content

Commit 43bd195

Browse files
feat(ILN): add in keepOpen configuration and tests (#301)
1 parent d30e5cc commit 43bd195

File tree

3 files changed

+202
-60
lines changed

3 files changed

+202
-60
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<nav role="navigation" aria-label="Interior Left Navigation" data-interior-left-nav class="bx--interior-left-nav bx--interior-left-nav--collapseable" data-keep-open="true">
2+
<ul role="menubar" class="left-nav-list" data-interior-left-nav-list aria-hidden="false">
3+
<li role="menuitem" tabindex="0" class="left-nav-list__item" data-interior-left-nav-item>
4+
<a class="left-nav-list__item-link">
5+
Example Item 1
6+
</a>
7+
</li>
8+
<li role="menuitem" tabindex="0" class="left-nav-list__item" data-interior-left-nav-item>
9+
<a class="left-nav-list__item-link">
10+
Example Item 2
11+
</a>
12+
</li>
13+
<li role="menuitem" tabindex="0" class="left-nav-list__item left-nav-list__item--has-children" data-interior-left-nav-item
14+
data-interior-left-nav-with-children>
15+
<a class="left-nav-list__item-link">
16+
Example Item 3
17+
<div class="left-nav-list__item-icon">
18+
<svg class="bx--interior-left-nav__icon" width="10" height="5" viewBox="0 0 10 5" fill-rule="evenodd">
19+
<path d="M10 0L5 5 0 0z"></path>
20+
</svg>
21+
</div>
22+
</a>
23+
<ul role="menu" aria-hidden="true" class="left-nav-list left-nav-list--nested" data-interior-left-nav-nested-list>
24+
<li class="left-nav-list__item" data-interior-left-nav-nested-item role="menuitem" tabindex="-1">
25+
<a href="#example-item-1A" class="left-nav-list__item-link" data-interior-left-nav-item-link tabindex="-1">Example Item 1A</a>
26+
</li>
27+
<li class="left-nav-list__item" data-interior-left-nav-nested-item role="menuitem" tabindex="-1">
28+
<a href="#example-item-1B" class="left-nav-list__item-link" data-interior-left-nav-item-link tabindex="-1">Example Item 1B</a>
29+
</li>
30+
<li class="left-nav-list__item" data-interior-left-nav-nested-item role="menuitem" tabindex="-1">
31+
<a href="#example-item-1C" class="left-nav-list__item-link" data-interior-left-nav-item-link tabindex="-1">Example Item 1C</a>
32+
</li>
33+
<li class="left-nav-list__item" data-interior-left-nav-nested-item role="menuitem" tabindex="-1">
34+
<a href="#example-item-1D" class="left-nav-list__item-link" data-interior-left-nav-item-link tabindex="-1">Example Item 1D</a>
35+
</li>
36+
</ul>
37+
</li>
38+
<li role="menuitem" tabindex="0" class="left-nav-list__item left-nav-list__item--has-children" data-interior-left-nav-item
39+
data-interior-left-nav-with-children>
40+
<a class="left-nav-list__item-link">
41+
Example Item 4
42+
<div class="left-nav-list__item-icon">
43+
<svg class="bx--interior-left-nav__icon" width="10" height="5" viewBox="0 0 10 5" fill-rule="evenodd">
44+
<path d="M10 0L5 5 0 0z"></path>
45+
</svg>
46+
</div>
47+
</a>
48+
<ul role="menu" aria-hidden="true" class="left-nav-list left-nav-list--nested" data-interior-left-nav-nested-list>
49+
<li class="left-nav-list__item" data-interior-left-nav-nested-item role="menuitem" tabindex="-1">
50+
<a href="#example-item-2A" class="left-nav-list__item-link" data-interior-left-nav-item-link tabindex="-1">Example Item 2A</a>
51+
</li>
52+
<li class="left-nav-list__item" data-interior-left-nav-nested-item role="menuitem" tabindex="-1">
53+
<a href="#example-item-2B" class="left-nav-list__item-link" data-interior-left-nav-item-link tabindex="-1">Example Item 2B</a>
54+
</li>
55+
<li class="left-nav-list__item" data-interior-left-nav-nested-item role="menuitem" tabindex="-1">
56+
<a href="#example-item-2C" class="left-nav-list__item-link" data-interior-left-nav-item-link tabindex="-1">Example Item 2C</a>
57+
</li>
58+
<li class="left-nav-list__item" data-interior-left-nav-nested-item role="menuitem" tabindex="-1">
59+
<a href="#example-item-2D" class="left-nav-list__item-link" data-interior-left-nav-item-link tabindex="-1">Example Item 2D</a>
60+
</li>
61+
</ul>
62+
</li>
63+
</ul>
64+
65+
<div class="bx--interior-left-nav-collapse" data-interior-left-nav-collapse>
66+
<a class="bx--interior-left-nav-collapse__link" href="#">
67+
<svg class="bx--interior-left-nav-collapse__arrow" width="8" height="12" viewBox="0 0 8 12" fill-rule="evenodd">
68+
<path d="M7.5 10.6L2.8 6l4.7-4.6L6.1 0 0 6l6.1 6z"></path>
69+
</svg>
70+
</a>
71+
</div>
72+
</nav>

src/components/interior-left-nav/interior-left-nav.js

Lines changed: 68 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import mixin from '../../globals/js/misc/mixin';
22
import createComponent from '../../globals/js/mixins/create-component';
3-
import initComponentBySearch
4-
from '../../globals/js/mixins/init-component-by-search';
3+
import initComponentBySearch from '../../globals/js/mixins/init-component-by-search';
54
import eventMatches from '../../globals/js/misc/event-matches';
65
import toggleClass from '../../globals/js/misc/svg-toggle-class';
76

@@ -16,31 +15,37 @@ class InteriorLeftNav extends mixin(createComponent, initComponentBySearch) {
1615
constructor(element, options) {
1716
super(element, options);
1817
this.constructor.components.set(this.element, this);
18+
19+
this.keepOpen =
20+
this.element.dataset.keepOpen === undefined
21+
? this.options.keepOpen
22+
: Boolean(this.element.dataset.keepOpen);
23+
1924
this.hookListItemsEvents();
2025
}
2126

2227
hookListItemsEvents = () => {
23-
this.element.addEventListener('click', (evt) => {
28+
this.element.addEventListener('click', evt => {
2429
const leftNavItem = eventMatches(
2530
evt,
26-
this.options.selectorLeftNavListItem,
31+
this.options.selectorLeftNavListItem
2732
);
2833
const collapseEl = eventMatches(
2934
evt,
30-
this.options.selectorLeftNavCollapse,
35+
this.options.selectorLeftNavCollapse
3136
);
3237
const collapsedBar = eventMatches(
3338
evt,
34-
`.${this.options.classLeftNavCollapsed}`,
39+
`.${this.options.classLeftNavCollapsed}`
3540
);
3641

3742
if (leftNavItem) {
3843
const childItem = eventMatches(
3944
evt,
40-
this.options.selectorLeftNavNestedListItem,
45+
this.options.selectorLeftNavNestedListItem
4146
);
4247
const hasChildren = leftNavItem.classList.contains(
43-
'left-nav-list__item--has-children',
48+
'left-nav-list__item--has-children'
4449
);
4550
if (childItem) {
4651
this.addActiveListItem(childItem);
@@ -57,14 +62,14 @@ class InteriorLeftNav extends mixin(createComponent, initComponentBySearch) {
5762
}
5863
});
5964

60-
this.element.addEventListener('keydown', (evt) => {
65+
this.element.addEventListener('keydown', evt => {
6166
const leftNavItemWithChildren = eventMatches(
6267
evt,
63-
this.options.selectorLeftNavListItemHasChildren,
68+
this.options.selectorLeftNavListItemHasChildren
6469
);
6570
const leftNavItem = eventMatches(
6671
evt,
67-
this.options.selectorLeftNavListItem,
72+
this.options.selectorLeftNavListItem
6873
);
6974

7075
if (leftNavItemWithChildren && evt.which === 13) {
@@ -78,16 +83,16 @@ class InteriorLeftNav extends mixin(createComponent, initComponentBySearch) {
7883
addActiveListItem(item) {
7984
[
8085
...this.element.querySelectorAll(this.options.selectorLeftNavListItem),
81-
].forEach((currentItem) => {
86+
].forEach(currentItem => {
8287
if (!(item === currentItem)) {
8388
currentItem.classList.remove(this.options.classActiveLeftNavListItem);
8489
}
8590
});
8691
[
8792
...this.element.querySelectorAll(
88-
this.options.selectorLeftNavNestedListItem,
93+
this.options.selectorLeftNavNestedListItem
8994
),
90-
].forEach((currentItem) => {
95+
].forEach(currentItem => {
9196
if (!(item === currentItem)) {
9297
currentItem.classList.remove(this.options.classActiveLeftNavListItem);
9398
}
@@ -103,39 +108,45 @@ class InteriorLeftNav extends mixin(createComponent, initComponentBySearch) {
103108
*/
104109
handleNestedListClick(listItem, evt) {
105110
const allNestedItems = [
106-
...document.querySelectorAll(
107-
this.options.selectorLeftNavListItemHasChildren,
111+
...this.element.querySelectorAll(
112+
this.options.selectorLeftNavListItemHasChildren
108113
),
109114
];
110115
const isOpen = listItem.classList.contains(
111-
this.options.classExpandedLeftNavListItem,
116+
this.options.classExpandedLeftNavListItem
112117
);
113-
allNestedItems.forEach((currentItem) => {
114-
if (currentItem !== listItem) {
115-
toggleClass(
116-
currentItem,
117-
this.options.classExpandedLeftNavListItem,
118-
false,
119-
);
120-
}
121-
});
122-
if (!('InteriorLeftNavItemLink' in evt.target.dataset)) {
123-
toggleClass(listItem, this.options.classExpandedLeftNavListItem, !isOpen);
124-
}
125118
const list = listItem.querySelector(this.options.selectorLeftNavNestedList);
126119
const listItems = [
127120
...list.querySelectorAll(this.options.selectorLeftNavNestedListItem),
128121
];
129-
listItems.forEach((item) => {
122+
123+
if (!this.keepOpen) {
124+
allNestedItems.forEach(currentItem => {
125+
if (currentItem !== listItem) {
126+
toggleClass(
127+
currentItem,
128+
this.options.classExpandedLeftNavListItem,
129+
false
130+
);
131+
}
132+
});
133+
}
134+
135+
if (!('InteriorLeftNavItemLink' in evt.target.dataset)) {
136+
toggleClass(listItem, this.options.classExpandedLeftNavListItem, !isOpen);
137+
}
138+
139+
// a11y
140+
listItems.forEach(item => {
130141
if (isOpen) {
131142
// eslint-disable-next-line no-param-reassign
132143
item.querySelector(
133-
this.options.selectorLeftNavListItemLink,
144+
this.options.selectorLeftNavListItemLink
134145
).tabIndex = -1;
135146
} else {
136147
// eslint-disable-next-line no-param-reassign
137148
item.querySelector(
138-
this.options.selectorLeftNavListItemLink,
149+
this.options.selectorLeftNavListItemLink
139150
).tabIndex = 0;
140151
}
141152
});
@@ -154,38 +165,32 @@ class InteriorLeftNav extends mixin(createComponent, initComponentBySearch) {
154165
this.element.dataset.collapsed = true;
155166
this.element.classList.add(this.options.classLeftNavCollapsing);
156167

157-
window.setTimeout(
158-
() => {
159-
this.element.classList.remove(this.options.classLeftNavCollapsing);
160-
this.element.classList.add(this.options.classLeftNavCollapsed);
161-
this.element.dispatchEvent(
162-
new CustomEvent(this.options.eventAfterLeftNavToggled, {
163-
bubbles: true,
164-
cancelable: true,
165-
detail: { collapsed: true },
166-
}),
167-
);
168-
},
169-
250,
170-
);
168+
window.setTimeout(() => {
169+
this.element.classList.remove(this.options.classLeftNavCollapsing);
170+
this.element.classList.add(this.options.classLeftNavCollapsed);
171+
this.element.dispatchEvent(
172+
new CustomEvent(this.options.eventAfterLeftNavToggled, {
173+
bubbles: true,
174+
cancelable: true,
175+
detail: { collapsed: true },
176+
})
177+
);
178+
}, 250);
171179
} else {
172180
this.element.dataset.collapsed = false;
173181
this.element.classList.remove(this.options.classLeftNavCollapsed);
174182
this.element.classList.add(this.options.classLeftNavExpanding);
175183

176-
window.setTimeout(
177-
() => {
178-
this.element.classList.remove(this.options.classLeftNavExpanding);
179-
this.element.dispatchEvent(
180-
new CustomEvent(this.options.eventAfterLeftNavToggled, {
181-
bubbles: true,
182-
cancelable: true,
183-
detail: { collapsed: false },
184-
}),
185-
);
186-
},
187-
250,
188-
);
184+
window.setTimeout(() => {
185+
this.element.classList.remove(this.options.classLeftNavExpanding);
186+
this.element.dispatchEvent(
187+
new CustomEvent(this.options.eventAfterLeftNavToggled, {
188+
bubbles: true,
189+
cancelable: true,
190+
detail: { collapsed: false },
191+
})
192+
);
193+
}, 250);
189194
}
190195
}
191196
};
@@ -214,7 +219,8 @@ class InteriorLeftNav extends mixin(createComponent, initComponentBySearch) {
214219
selectorLeftNavListItem: '[data-interior-left-nav-item]',
215220
selectorLeftNavListItemLink: '[data-interior-left-nav-item-link]',
216221
selectorLeftNavNestedListItem: '[data-interior-left-nav-nested-item]',
217-
selectorLeftNavListItemHasChildren: '[data-interior-left-nav-with-children]',
222+
selectorLeftNavListItemHasChildren:
223+
'[data-interior-left-nav-with-children]',
218224
selectorLeftNavCollapse: '[data-interior-left-nav-collapse]',
219225
// CSS Class Selectors
220226
classActiveLeftNavListItem: 'left-nav-list__item--active',
@@ -225,6 +231,8 @@ class InteriorLeftNav extends mixin(createComponent, initComponentBySearch) {
225231
// Event
226232
eventBeforeLeftNavToggled: 'left-nav-beingtoggled',
227233
eventAfterLeftNavToggled: 'left-nav-toggled',
234+
// Option
235+
keepOpen: false,
228236
};
229237
}
230238

tests/spec/interior-left-nav_spec.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import InteriorLeftNav
33
from '../../src/components/interior-left-nav/interior-left-nav';
44
import InteriorLeftNavHtml
55
from '../../src/components/interior-left-nav/interior-left-nav.html';
6+
import KeepOpen from '../../src/components/interior-left-nav/interior-left-nav-keep-open.html';
67

78
describe('Test interior left nav', function () {
89
describe('Constructor', function () {
@@ -45,6 +46,67 @@ describe('Test interior left nav', function () {
4546
});
4647
});
4748

49+
describe('keepOpen', function () {
50+
let container;
51+
let element;
52+
let instance1;
53+
let instance2;
54+
let instance3;
55+
56+
beforeEach(function () {
57+
container = document.createElement('div');
58+
container.innerHTML = InteriorLeftNavHtml;
59+
document.body.appendChild(container);
60+
element = document.querySelector('[data-interior-left-nav]');
61+
});
62+
63+
it('Should have a default setting of false', function () {
64+
instance1 = new InteriorLeftNav(element);
65+
expect(instance1.options.keepOpen).to.be.false;
66+
document.body.removeChild(container);
67+
instance1.release();
68+
});
69+
70+
it('Should accept an option in the constructor to override', function () {
71+
instance2 = new InteriorLeftNav(element, {
72+
keepOpen: true,
73+
});
74+
expect(instance2.options.keepOpen).to.be.true;
75+
document.body.removeChild(container);
76+
instance2.release();
77+
});
78+
79+
it('Should read data-keep-open attribute in the markup to override', function () {
80+
document.body.removeChild(container);
81+
const container2 = document.createElement('div');
82+
container2.innerHTML = KeepOpen;
83+
document.body.appendChild(container2);
84+
element = document.querySelector('[data-interior-left-nav]');
85+
instance3 = new InteriorLeftNav(element);
86+
expect(instance3.keepOpen).to.be.true;
87+
document.body.removeChild(container2);
88+
instance3.release();
89+
});
90+
91+
it('If true, should not remove expanded class from other elements on second open', function () {
92+
document.body.removeChild(container);
93+
const container2 = document.createElement('div');
94+
container2.innerHTML = KeepOpen;
95+
document.body.appendChild(container2);
96+
element = document.querySelector('[data-interior-left-nav]');
97+
instance3 = new InteriorLeftNav(element);
98+
99+
const nested = document.querySelectorAll('.left-nav-list__item--has-children');
100+
101+
for (let i = 0; i < nested.length; i += 1) {
102+
nested[i].dispatchEvent(new CustomEvent('click', { bubbles: true }));
103+
}
104+
105+
const expanded = document.querySelectorAll('.left-nav-list__item--expanded');
106+
expect(expanded.length).to.equal(nested.length);
107+
});
108+
});
109+
48110
describe('addActiveListItem', function () {
49111
let container;
50112
let element;

0 commit comments

Comments
 (0)