Skip to content

Commit 8641ad2

Browse files
authoredSep 10, 2021
fix(masthead): handle overflow tabbing (#7085)
### Related Ticket(s) #5894 ### Description Overflow with tabbing for masthead and table of contents in LTR and RTL. ### Changelog **New** - {{new thing}} **Changed** - {{changed thing}} **Removed** - {{removed thing}} <!-- React and Web Component deploy previews are enabled by default. --> <!-- To enable additional available deploy previews, apply the following --> <!-- labels for the corresponding package: --> <!-- *** "package: services": Services --> <!-- *** "package: utilities": Utilities --> <!-- *** "package: styles": Carbon Expressive --> <!-- *** "RTL": React / Web Components (RTL) --> <!-- *** "feature flag": React / Web Components (experimental) -->
1 parent ac46dc5 commit 8641ad2

File tree

3 files changed

+137
-35
lines changed

3 files changed

+137
-35
lines changed
 

‎packages/web-components/src/components/masthead/__tests__/top-nav.test.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ describe('dds-top-nav', function() {
7777
await Promise.resolve();
7878
(topNav!.shadowRoot!.querySelector('[part="next-button"]') as HTMLElement).click();
7979
await Promise.resolve();
80-
expect((topNav!.shadowRoot!.querySelector('.bx--header__nav-content') as HTMLElement).style.left).toBe('-175px');
80+
expect((topNav!.shadowRoot!.querySelector('.bx--header__nav-content') as HTMLElement).style.left).toBe('-167px');
8181
});
8282

8383
it('should support snapping to menu item', async function() {
@@ -90,7 +90,7 @@ describe('dds-top-nav', function() {
9090
await Promise.resolve();
9191
(topNav!.shadowRoot!.querySelector('[part="next-button"]') as HTMLElement).click();
9292
await Promise.resolve();
93-
expect((topNav!.shadowRoot!.querySelector('.bx--header__nav-content') as HTMLElement).style.left).toBe('-100px');
93+
expect((topNav!.shadowRoot!.querySelector('.bx--header__nav-content') as HTMLElement).style.left).toBe('-92px');
9494
});
9595

9696
it('should cope with change in the hidden state of the go to next page button', async function() {
@@ -104,7 +104,7 @@ describe('dds-top-nav', function() {
104104
(topNav as any)._currentScrollPosition = 565;
105105
(topNav!.shadowRoot!.querySelector('[part="next-button"]') as HTMLElement).click();
106106
await Promise.resolve();
107-
expect((topNav!.shadowRoot!.querySelector('.bx--header__nav-content') as HTMLElement).style.left).toBe('-700px');
107+
expect((topNav!.shadowRoot!.querySelector('.bx--header__nav-content') as HTMLElement).style.left).toBe('-692px');
108108
});
109109

110110
it('should snap to the right edge at the last page', async function() {
@@ -138,7 +138,7 @@ describe('dds-top-nav', function() {
138138
(topNav as any)._currentScrollPosition = 350;
139139
(topNav!.shadowRoot!.querySelector('[part="prev-button"]') as HTMLElement).click();
140140
await Promise.resolve();
141-
expect((topNav!.shadowRoot!.querySelector('.bx--header__nav-content') as HTMLElement).style.left).toBe('-175px');
141+
expect((topNav!.shadowRoot!.querySelector('.bx--header__nav-content') as HTMLElement).style.left).toBe('-183px');
142142
});
143143

144144
it('should support snapping to menu item', async function() {
@@ -156,7 +156,7 @@ describe('dds-top-nav', function() {
156156
(topNav!.shadowRoot!.querySelector('[part="prev-button"]') as HTMLElement).click();
157157
await Promise.resolve();
158158
// Given the 4th item should be the right-most, the left position should be `350px - (200px - 80px)`
159-
expect((topNav!.shadowRoot!.querySelector('.bx--header__nav-content') as HTMLElement).style.left).toBe('-230px');
159+
expect((topNav!.shadowRoot!.querySelector('.bx--header__nav-content') as HTMLElement).style.left).toBe('-238px');
160160
});
161161

162162
it('should cope with change in the hidden state of the go to next page button', async function() {
@@ -170,7 +170,7 @@ describe('dds-top-nav', function() {
170170
(topNav as any)._currentScrollPosition = 700; // The scrolling position of the last page
171171
(topNav!.shadowRoot!.querySelector('[part="prev-button"]') as HTMLElement).click();
172172
await Promise.resolve();
173-
expect((topNav!.shadowRoot!.querySelector('.bx--header__nav-content') as HTMLElement).style.left).toBe('-565px');
173+
expect((topNav!.shadowRoot!.querySelector('.bx--header__nav-content') as HTMLElement).style.left).toBe('-573px');
174174
});
175175

176176
it('should snap to the left edge at the first page', async function() {
@@ -189,7 +189,7 @@ describe('dds-top-nav', function() {
189189
(topNav as any)._currentScrollPosition = 110;
190190
(topNav!.shadowRoot!.querySelector('[part="prev-button"]') as HTMLElement).click();
191191
await Promise.resolve();
192-
expect((topNav!.shadowRoot!.querySelector('.bx--header__nav-content') as HTMLElement).style.left).toBe('0px');
192+
expect((topNav!.shadowRoot!.querySelector('.bx--header__nav-content') as HTMLElement).style.left).toBe('-48px');
193193
});
194194
});
195195

‎packages/web-components/src/components/masthead/top-nav.ts

+85-15
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ import styles from './masthead.scss';
2323
const { prefix } = settings;
2424
const { stablePrefix: ddsPrefix } = ddsSettings;
2525

26+
// button gradient width size
27+
const buttonGradientWidth = 8;
28+
2629
/**
2730
* @param a An array.
2831
* @param predicate The callback function.
@@ -199,30 +202,43 @@ class DDSTopNav extends StableSelectorMixin(HostListenerMixin(BXHeaderNav)) {
199202
// If the right-side intersection sentinel is in the view, it means that right-side caret button is hidden.
200203
// Given scrolling to left makes it shown,
201204
// `contentContainerNode!.offsetWidth` will shrink as we scroll and we need to adjust for it.
202-
const caretRightNodeWidthAdjustment = isIntersectionRightTrackerInContent ? caretRightNode!.offsetWidth : 0;
205+
203206
const elems = slotNode?.assignedElements() as HTMLElement[];
204207
if (elems) {
205208
if (pageIsRTL) {
209+
const caretLeftNodeWidthAdjustment = this._isIntersectionLeftTrackerInContent ? caretLeftNode!.offsetWidth : 0;
206210
const navRight = navNode!.getBoundingClientRect().right;
207211
const lastVisibleElementIndex = elems.findIndex(
208-
elem => elem.getBoundingClientRect().left < navRight - currentScrollPosition - caretRightNodeWidthAdjustment
212+
elem => elem.getBoundingClientRect().left < navRight - currentScrollPosition - caretRightNode!.offsetWidth
209213
);
210214
if (lastVisibleElementIndex >= 0) {
211-
this._currentScrollPosition =
215+
this._currentScrollPosition = Math.max(
212216
navRight -
213-
elems[lastVisibleElementIndex].getBoundingClientRect().left -
214-
contentContainerNode!.offsetWidth +
215-
caretRightNode!.offsetWidth;
217+
elems[lastVisibleElementIndex].getBoundingClientRect().left -
218+
contentContainerNode!.offsetWidth +
219+
caretLeftNodeWidthAdjustment +
220+
caretRightNode!.offsetWidth +
221+
buttonGradientWidth,
222+
0
223+
);
216224
}
217225
} else {
226+
const caretRightNodeWidthAdjustment = isIntersectionRightTrackerInContent
227+
? caretRightNode!.offsetWidth + buttonGradientWidth
228+
: buttonGradientWidth;
229+
const caretLeftNodeWidthAdjustment = this._isIntersectionLeftTrackerInContent
230+
? caretLeftNode!.offsetWidth + buttonGradientWidth
231+
: 0;
218232
const navLeft = navNode!.getBoundingClientRect().left;
219233
const lastVisibleElementIndex = findLastIndex(
220234
elems,
221235
elem => elem.getBoundingClientRect().left - navLeft < currentScrollPosition
222236
);
223237
if (lastVisibleElementIndex >= 0) {
224238
const lastVisibleElementRight = elems[lastVisibleElementIndex].getBoundingClientRect().right - navLeft;
225-
const newScrollPosition = lastVisibleElementRight - (contentContainerNode!.offsetWidth - caretRightNodeWidthAdjustment);
239+
const newScrollPosition =
240+
lastVisibleElementRight -
241+
(contentContainerNode!.offsetWidth + caretLeftNodeWidthAdjustment - caretRightNodeWidthAdjustment);
226242
// If the new scroll position is less than the width of the left caret button,
227243
// it means that hiding the left caret button reveals the whole of the left-most nav item.
228244
// Snaps the left-most nav item to the left edge of nav container in this case.
@@ -247,39 +263,92 @@ class DDSTopNav extends StableSelectorMixin(HostListenerMixin(BXHeaderNav)) {
247263
_pageIsRTL: pageIsRTL,
248264
_slotNode: slotNode,
249265
} = this;
250-
const caretLeftNodeWidthAdjustment = isIntersectionLeftTrackerInContent ? caretLeftNode!.offsetWidth : 0;
251-
const caretRightNodeWidthAdjustment = caretRightNode!.offsetWidth;
252266
const interimLeft = currentScrollPosition + contentContainerNode!.offsetWidth;
253267
const elems = slotNode?.assignedElements() as HTMLElement[];
254268
if (elems) {
255269
if (pageIsRTL) {
256270
const navRight = navNode!.getBoundingClientRect().right;
257271
const firstVisibleElementIndex = elems.findIndex(
258-
elem => navRight - elem.getBoundingClientRect().left > interimLeft - caretRightNodeWidthAdjustment
272+
elem => navRight - elem.getBoundingClientRect().left > interimLeft - caretLeftNode!.offsetWidth - buttonGradientWidth
259273
);
260274
if (firstVisibleElementIndex > 0) {
261275
const firstVisibleElementLeft = Math.abs(
262-
elems[firstVisibleElementIndex].getBoundingClientRect().right - navRight + caretRightNodeWidthAdjustment
276+
elems[firstVisibleElementIndex].getBoundingClientRect().right -
277+
navRight +
278+
caretLeftNode!.offsetWidth +
279+
buttonGradientWidth
263280
);
264281
const maxLeft = contentNode!.scrollWidth - contentContainerNode!.offsetWidth;
265282
this._currentScrollPosition = Math.min(firstVisibleElementLeft, maxLeft);
266283
}
267284
} else {
285+
const caretLeftNodeWidthAdjustment = isIntersectionLeftTrackerInContent ? caretLeftNode!.offsetWidth : 0;
268286
const navLeft = navNode!.getBoundingClientRect().left;
269287
const firstVisibleElementIndex = elems.findIndex(elem => elem.getBoundingClientRect().right - navLeft > interimLeft);
270288
if (firstVisibleElementIndex > 0) {
271-
const firstVisibleElementLeft = elems[firstVisibleElementIndex].getBoundingClientRect().left - navLeft;
289+
const firstVisibleElementLeft =
290+
elems[firstVisibleElementIndex].getBoundingClientRect().left - navLeft - buttonGradientWidth;
272291
// Ensures that is there is no blank area at the right hand side in scroll area
273292
// if we see the right remainder nav items can be contained in a page
274293
const maxLeft =
275294
contentNode!.scrollWidth -
276-
(contentContainerNode!.offsetWidth - caretLeftNodeWidthAdjustment + caretRightNodeWidthAdjustment);
295+
(contentContainerNode!.offsetWidth - caretLeftNodeWidthAdjustment + caretRightNode!.offsetWidth);
277296
this._currentScrollPosition = Math.min(firstVisibleElementLeft, maxLeft);
278297
}
279298
}
280299
}
281300
}
282301

302+
protected _handleOnKeyDown(event: KeyboardEvent) {
303+
const target = event.target as HTMLAnchorElement;
304+
const {
305+
_pageIsRTL: pageIsRTL,
306+
_navNode: navNode,
307+
_currentScrollPosition: currentScrollPosition,
308+
_contentContainerNode: contentContainerNode,
309+
_caretRightNode: caretRightNode,
310+
} = this;
311+
if (target) {
312+
if (pageIsRTL) {
313+
if (event.key === 'Tab') {
314+
if (event.shiftKey) {
315+
if (
316+
target.previousElementSibling &&
317+
caretRightNode &&
318+
target.previousElementSibling.getBoundingClientRect().right + currentScrollPosition >
319+
navNode!.getBoundingClientRect().right - caretRightNode.offsetWidth
320+
) {
321+
this._paginateLeft();
322+
}
323+
} else if (
324+
target.nextElementSibling &&
325+
caretRightNode &&
326+
navNode!.getBoundingClientRect().right - target.nextElementSibling.getBoundingClientRect().left >
327+
currentScrollPosition + contentContainerNode!.offsetWidth - caretRightNode.offsetWidth
328+
) {
329+
this._paginateRight();
330+
}
331+
}
332+
} else if (event.key === 'Tab') {
333+
if (event.shiftKey) {
334+
if (
335+
target.previousElementSibling &&
336+
target.previousElementSibling.getBoundingClientRect().left - navNode!.getBoundingClientRect().left <
337+
currentScrollPosition
338+
) {
339+
this._paginateLeft();
340+
}
341+
} else if (
342+
target.nextElementSibling &&
343+
Math.floor(target.nextElementSibling.getBoundingClientRect().right - navNode!.getBoundingClientRect().left) >
344+
currentScrollPosition + contentContainerNode!.offsetWidth
345+
) {
346+
this._paginateRight();
347+
}
348+
}
349+
}
350+
}
351+
283352
/**
284353
* Handles toggle event from the search component.
285354
*
@@ -329,6 +398,7 @@ class DDSTopNav extends StableSelectorMixin(HostListenerMixin(BXHeaderNav)) {
329398
_paginateLeft: paginateLeft,
330399
_paginateRight: paginateRight,
331400
_pageIsRTL: pageIsRTL,
401+
_handleOnKeyDown: handleOnKeyDown,
332402
} = this;
333403
const caretLeftContainerClasses = classMap({
334404
[`${prefix}--header__nav-caret-left-container`]: true,
@@ -366,7 +436,7 @@ class DDSTopNav extends StableSelectorMixin(HostListenerMixin(BXHeaderNav)) {
366436
class="${prefix}--header__menu-bar"
367437
aria-label="${ifNonNull(this.menuBarLabel)}"
368438
>
369-
<slot></slot>
439+
<slot @keydown="${handleOnKeyDown}"></slot>
370440
</ul>
371441
<div class="${prefix}--sub-content-left"></div>
372442
</nav>
@@ -408,7 +478,7 @@ class DDSTopNav extends StableSelectorMixin(HostListenerMixin(BXHeaderNav)) {
408478
class="${prefix}--header__menu-bar"
409479
aria-label="${ifNonNull(this.menuBarLabel)}"
410480
>
411-
<slot></slot>
481+
<slot @keydown="${handleOnKeyDown}"></slot>
412482
</ul>
413483
<div class="${prefix}--sub-content-right"></div>
414484
</nav>

‎packages/web-components/src/components/table-of-contents/table-of-contents.ts

+45-13
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ import StableSelectorMixin from '../../globals/mixins/stable-selector';
2525

2626
const { prefix } = settings;
2727
const { stablePrefix: ddsPrefix } = ddsSettings;
28+
29+
// total button width - grid offset
30+
const buttonWidthOffset = 32;
31+
2832
interface Cancelable {
2933
cancel(): void;
3034
}
@@ -216,21 +220,39 @@ class DDSTableOfContents extends HostListenerMixin(StableSelectorMixin(LitElemen
216220
private _handleOnKeyDown(event: KeyboardEvent) {
217221
const { selectorDesktopItem } = this.constructor as typeof DDSTableOfContents;
218222
const target = event.target as HTMLAnchorElement;
223+
const { _pageIsRTL: pageIsRTL } = this;
219224
if (target.matches?.(selectorDesktopItem)) {
220-
if (event.key === 'Tab') {
225+
if (pageIsRTL) {
226+
if (event.key === 'Tab') {
227+
if (event.shiftKey) {
228+
if (
229+
target.parentElement?.previousElementSibling &&
230+
target.parentElement?.previousElementSibling.getBoundingClientRect().right >
231+
this._navBar!.getBoundingClientRect().right - buttonWidthOffset
232+
) {
233+
this._paginateLeft();
234+
}
235+
} else if (
236+
target.parentElement?.nextElementSibling &&
237+
target.parentElement?.nextElementSibling.getBoundingClientRect().left <
238+
this._navBar!.getBoundingClientRect().left + buttonWidthOffset
239+
) {
240+
this._paginateRight();
241+
}
242+
}
243+
} else if (event.key === 'Tab') {
221244
if (event.shiftKey) {
222-
// 32 = total button width - grid offset
223245
if (
224246
target.parentElement?.previousElementSibling &&
225247
target.parentElement?.previousElementSibling!.getBoundingClientRect().left <
226-
this._navBar!.getBoundingClientRect().left + 32
248+
this._navBar!.getBoundingClientRect().left + buttonWidthOffset
227249
) {
228250
this._paginateLeft();
229251
}
230252
} else if (
231253
target.parentElement?.nextElementSibling &&
232254
target.parentElement?.nextElementSibling!.getBoundingClientRect().right >
233-
this._navBar!.getBoundingClientRect().right - 32
255+
this._navBar!.getBoundingClientRect().right - buttonWidthOffset
234256
) {
235257
this._paginateRight();
236258
}
@@ -387,22 +409,25 @@ class DDSTableOfContents extends HostListenerMixin(StableSelectorMixin(LitElemen
387409
if (elems) {
388410
if (pageIsRTL) {
389411
const interimLeft = navBar!.getBoundingClientRect().right;
390-
const lastVisibleElementIndex = findLastIndex(elems, elem => elem.getBoundingClientRect().right > interimLeft - 32);
412+
const lastVisibleElementIndex = findLastIndex(
413+
elems,
414+
elem => elem.getBoundingClientRect().right > interimLeft - buttonWidthOffset
415+
);
391416
if (lastVisibleElementIndex >= 0) {
392417
const lastVisibleElementRight = elems[lastVisibleElementIndex].getBoundingClientRect().left;
393418
// 48 = button width - button gradient
394419
const newScrollPosition = currentScrollPosition - lastVisibleElementRight + 48;
395420
this._currentScrollPosition = newScrollPosition <= 0 ? 0 : newScrollPosition;
396421
}
397422
} else {
398-
// 32 = total button width - grid offset
399423
const lastVisibleElementIndex = findLastIndex(
400424
elems,
401-
elem => elem.getBoundingClientRect().left < 32 + navBar!.getBoundingClientRect().left
425+
elem => elem.getBoundingClientRect().left < buttonWidthOffset + navBar!.getBoundingClientRect().left
402426
);
403427
if (lastVisibleElementIndex >= 0) {
404428
const lastVisibleElementRight = elems[lastVisibleElementIndex].getBoundingClientRect().right;
405-
const newScrollPosition = lastVisibleElementRight + currentScrollPosition - navBar!.getBoundingClientRect().right + 32;
429+
const newScrollPosition =
430+
lastVisibleElementRight + currentScrollPosition - navBar!.getBoundingClientRect().right + buttonWidthOffset;
406431
// If the new scroll position is less than the width of the left caret button,
407432
// it means that hiding the left caret button reveals the whole of the left-most nav item.
408433
// Snaps the left-most nav item to the left edge of nav container in this case.
@@ -428,21 +453,28 @@ class DDSTableOfContents extends HostListenerMixin(StableSelectorMixin(LitElemen
428453
if (elems) {
429454
if (pageIsRTL) {
430455
const interimLeft = navBar!.getBoundingClientRect().left;
431-
const firstVisibleElementIndex = elems.findIndex(elem => elem.getBoundingClientRect().left < interimLeft + 32);
456+
const firstVisibleElementIndex = elems.findIndex(
457+
elem => elem.getBoundingClientRect().left < interimLeft + buttonWidthOffset
458+
);
432459
if (firstVisibleElementIndex > 0) {
433460
const firstVisibleElementLeft = Math.abs(
434-
elems[firstVisibleElementIndex].getBoundingClientRect().right + 32 - navBar!.getBoundingClientRect().right
461+
elems[firstVisibleElementIndex].getBoundingClientRect().right +
462+
buttonWidthOffset -
463+
navBar!.getBoundingClientRect().right
435464
);
436465
const maxLeft = contentNode!.scrollWidth - navBar!.offsetWidth;
437466
this._currentScrollPosition = Math.min(firstVisibleElementLeft + currentScrollPosition, maxLeft);
438467
}
439468
} else {
440469
const interimRight = navBar!.getBoundingClientRect().right;
441-
// 32 = total button width - grid offset
442-
const firstVisibleElementIndex = elems.findIndex(elem => elem.getBoundingClientRect().right > interimRight - 32);
470+
const firstVisibleElementIndex = elems.findIndex(
471+
elem => elem.getBoundingClientRect().right > interimRight - buttonWidthOffset
472+
);
443473
if (firstVisibleElementIndex > 0) {
444474
const firstVisibleElementLeft =
445-
elems[firstVisibleElementIndex].getBoundingClientRect().left - navBar!.getBoundingClientRect().left - 32;
475+
elems[firstVisibleElementIndex].getBoundingClientRect().left -
476+
navBar!.getBoundingClientRect().left -
477+
buttonWidthOffset;
446478
// Ensures that is there is no blank area at the right hand side in scroll area
447479
// if we see the right remainder nav items can be contained in a page
448480
const maxLeft = contentNode!.scrollWidth - navBar!.offsetWidth;

0 commit comments

Comments
 (0)
Please sign in to comment.