Skip to content

Commit 7449a5a

Browse files
committed
Start docs redesign, new page metadata fields
1 parent 4989565 commit 7449a5a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+624
-79
lines changed

js/src/nav-overflow.js

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,22 @@ const SELECTOR_NAV_ITEM = '.nav-item'
2828
const SELECTOR_NAV_LINK = '.nav-link'
2929
const SELECTOR_OVERFLOW_TOGGLE = '.nav-overflow-toggle'
3030
const SELECTOR_OVERFLOW_MENU = '.nav-overflow-menu'
31+
const SELECTOR_CUSTOM_ICON = '[data-bs-overflow-icon]'
3132
const CLASS_NAME_KEEP = 'nav-overflow-keep'
3233

3334
const Default = {
35+
collapseBelow: 0,
36+
iconPlacement: 'start',
37+
menuPlacement: 'bottom-end',
3438
moreText: 'More',
3539
moreIcon: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3m5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3m5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3"/></svg>',
3640
threshold: 0 // Minimum items to keep visible before showing overflow
3741
}
3842

3943
const DefaultType = {
44+
collapseBelow: '(number|string)',
45+
iconPlacement: 'string',
46+
menuPlacement: 'string',
4047
moreText: 'string',
4148
moreIcon: 'string',
4249
threshold: 'number'
@@ -55,6 +62,7 @@ class NavOverflow extends BaseComponent {
5562
this._overflowMenu = null
5663
this._overflowToggle = null
5764
this._resizeObserver = null
65+
this._collapseBelow = 0
5866
this._isInitialized = false
5967

6068
this._init()
@@ -108,6 +116,9 @@ class NavOverflow extends BaseComponent {
108116
item.dataset.bsNavOrder = index
109117
}
110118

119+
// Resolve collapseBelow threshold once
120+
this._collapseBelow = this._resolveCollapseBelow()
121+
111122
// Create overflow menu if it doesn't exist
112123
this._createOverflowMenu()
113124

@@ -129,12 +140,18 @@ class NavOverflow extends BaseComponent {
129140
return
130141
}
131142

143+
const iconHtml = this._resolveIcon()
144+
const iconSpan = `<span class="nav-overflow-icon">${iconHtml}</span>`
145+
const textSpan = `<span class="nav-overflow-text">${this._config.moreText}</span>`
146+
const toggleContent = this._config.iconPlacement === 'end' ?
147+
`${textSpan}${iconSpan}` :
148+
`${iconSpan}${textSpan}`
149+
132150
const overflowItem = document.createElement('li')
133151
overflowItem.className = 'nav-item nav-overflow-item'
134152
overflowItem.innerHTML = `
135-
<button class="nav-link nav-overflow-toggle" type="button" data-bs-toggle="menu" data-bs-placement="bottom-end" aria-expanded="false">
136-
<span class="nav-overflow-icon">${this._config.moreIcon}</span>
137-
<span class="nav-overflow-text">${this._config.moreText}</span>
153+
<button class="nav-link nav-overflow-toggle" type="button" data-bs-toggle="menu" data-bs-placement="${this._config.menuPlacement}" aria-expanded="false">
154+
${toggleContent}
138155
</button>
139156
<div class="${CLASS_NAME_OVERFLOW_MENU} menu"></div>
140157
`
@@ -144,6 +161,38 @@ class NavOverflow extends BaseComponent {
144161
this._overflowMenu = overflowItem.querySelector(SELECTOR_OVERFLOW_MENU)
145162
}
146163

164+
_resolveIcon() {
165+
const customIconElement = SelectorEngine.findOne(SELECTOR_CUSTOM_ICON, this._element)
166+
167+
if (!customIconElement) {
168+
return this._config.moreIcon
169+
}
170+
171+
const iconClone = customIconElement.cloneNode(true)
172+
iconClone.removeAttribute('data-bs-overflow-icon')
173+
const iconHtml = iconClone.outerHTML
174+
175+
customIconElement.remove()
176+
177+
return iconHtml
178+
}
179+
180+
_resolveCollapseBelow() {
181+
const value = this._config.collapseBelow
182+
183+
if (typeof value === 'number') {
184+
return value
185+
}
186+
187+
if (typeof value === 'string' && value !== '') {
188+
const cssValue = getComputedStyle(document.documentElement)
189+
.getPropertyValue(`--bs-breakpoint-${value}`)
190+
return parseFloat(cssValue) || 0
191+
}
192+
193+
return 0
194+
}
195+
147196
_setupResizeObserver() {
148197
if (typeof ResizeObserver === 'undefined') {
149198
// Fallback for older browsers
@@ -164,6 +213,33 @@ class NavOverflow extends BaseComponent {
164213

165214
const navWidth = this._element.offsetWidth
166215
const overflowItem = this._overflowToggle?.closest('.nav-item')
216+
217+
// When below the collapseBelow threshold, force all items into overflow
218+
if (this._collapseBelow > 0 && navWidth < this._collapseBelow) {
219+
const itemsToOverflow = this._items.filter(
220+
item => !item.classList.contains(CLASS_NAME_KEEP)
221+
)
222+
223+
this._moveToOverflow(itemsToOverflow)
224+
225+
if (overflowItem) {
226+
if (itemsToOverflow.length > 0) {
227+
overflowItem.classList.remove(CLASS_NAME_HIDDEN)
228+
} else {
229+
overflowItem.classList.add(CLASS_NAME_HIDDEN)
230+
}
231+
}
232+
233+
if (itemsToOverflow.length > 0) {
234+
EventHandler.trigger(this._element, EVENT_OVERFLOW, {
235+
overflowCount: itemsToOverflow.length,
236+
visibleCount: this._items.length - itemsToOverflow.length
237+
})
238+
}
239+
240+
return
241+
}
242+
167243
const overflowWidth = overflowItem?.offsetWidth || 0
168244

169245
let usedWidth = 0

js/tests/unit/nav-overflow.spec.js

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,11 +514,223 @@ describe('NavOverflow', () => {
514514

515515
it('should have correct DefaultType', () => {
516516
expect(NavOverflow.DefaultType).toEqual(jasmine.objectContaining({
517+
collapseBelow: '(number|string)',
518+
iconPlacement: 'string',
519+
menuPlacement: 'string',
517520
moreText: 'string',
518521
moreIcon: 'string',
519522
threshold: 'number'
520523
}))
521524
})
525+
526+
it('should respect custom menuPlacement option', () => {
527+
fixtureEl.innerHTML = [
528+
'<ul class="nav" data-bs-toggle="nav-overflow">',
529+
' <li class="nav-item"><a class="nav-link" href="#">Link 1</a></li>',
530+
'</ul>'
531+
].join('')
532+
533+
const navEl = fixtureEl.querySelector('[data-bs-toggle="nav-overflow"]')
534+
const navOverflow = new NavOverflow(navEl, {
535+
menuPlacement: 'bottom-start'
536+
})
537+
538+
const toggle = navEl.querySelector('.nav-overflow-toggle')
539+
expect(toggle.getAttribute('data-bs-placement')).toEqual('bottom-start')
540+
541+
navOverflow.dispose()
542+
})
543+
544+
it('should use a child element with [data-bs-overflow-icon] as the icon', () => {
545+
fixtureEl.innerHTML = [
546+
'<ul class="nav" data-bs-toggle="nav-overflow">',
547+
' <li class="nav-item"><a class="nav-link" href="#">Link 1</a></li>',
548+
' <svg data-bs-overflow-icon class="bi-chevron" width="16" height="16"><circle cx="8" cy="8" r="8"/></svg>',
549+
'</ul>'
550+
].join('')
551+
552+
const navEl = fixtureEl.querySelector('[data-bs-toggle="nav-overflow"]')
553+
const navOverflow = new NavOverflow(navEl)
554+
555+
const iconContainer = navEl.querySelector('.nav-overflow-icon')
556+
const svg = iconContainer.querySelector('svg')
557+
expect(svg).not.toBeNull()
558+
expect(svg).toHaveClass('bi-chevron')
559+
expect(svg.hasAttribute('data-bs-overflow-icon')).toBeFalse()
560+
561+
// Original element should be removed from the nav
562+
expect(navEl.querySelector('[data-bs-overflow-icon]')).toBeNull()
563+
564+
navOverflow.dispose()
565+
})
566+
567+
it('should prefer child [data-bs-overflow-icon] over moreIcon config', () => {
568+
fixtureEl.innerHTML = [
569+
'<ul class="nav" data-bs-toggle="nav-overflow">',
570+
' <li class="nav-item"><a class="nav-link" href="#">Link 1</a></li>',
571+
' <svg data-bs-overflow-icon class="from-markup" width="16" height="16"><circle cx="8" cy="8" r="8"/></svg>',
572+
'</ul>'
573+
].join('')
574+
575+
const navEl = fixtureEl.querySelector('[data-bs-toggle="nav-overflow"]')
576+
const navOverflow = new NavOverflow(navEl, {
577+
moreIcon: '<span class="from-config">X</span>'
578+
})
579+
580+
const iconContainer = navEl.querySelector('.nav-overflow-icon')
581+
expect(iconContainer.querySelector('.from-markup')).not.toBeNull()
582+
expect(iconContainer.querySelector('.from-config')).toBeNull()
583+
584+
navOverflow.dispose()
585+
})
586+
587+
it('should place icon after text when iconPlacement is "end"', () => {
588+
fixtureEl.innerHTML = [
589+
'<ul class="nav" data-bs-toggle="nav-overflow">',
590+
' <li class="nav-item"><a class="nav-link" href="#">Link 1</a></li>',
591+
'</ul>'
592+
].join('')
593+
594+
const navEl = fixtureEl.querySelector('[data-bs-toggle="nav-overflow"]')
595+
const navOverflow = new NavOverflow(navEl, {
596+
iconPlacement: 'end'
597+
})
598+
599+
const toggle = navEl.querySelector('.nav-overflow-toggle')
600+
const children = [...toggle.children]
601+
const textIndex = children.findIndex(el => el.classList.contains('nav-overflow-text'))
602+
const iconIndex = children.findIndex(el => el.classList.contains('nav-overflow-icon'))
603+
604+
expect(textIndex).toBeLessThan(iconIndex)
605+
606+
navOverflow.dispose()
607+
})
608+
609+
it('should place icon before text by default (iconPlacement "start")', () => {
610+
fixtureEl.innerHTML = [
611+
'<ul class="nav" data-bs-toggle="nav-overflow">',
612+
' <li class="nav-item"><a class="nav-link" href="#">Link 1</a></li>',
613+
'</ul>'
614+
].join('')
615+
616+
const navEl = fixtureEl.querySelector('[data-bs-toggle="nav-overflow"]')
617+
const navOverflow = new NavOverflow(navEl)
618+
619+
const toggle = navEl.querySelector('.nav-overflow-toggle')
620+
const children = [...toggle.children]
621+
const textIndex = children.findIndex(el => el.classList.contains('nav-overflow-text'))
622+
const iconIndex = children.findIndex(el => el.classList.contains('nav-overflow-icon'))
623+
624+
expect(iconIndex).toBeLessThan(textIndex)
625+
626+
navOverflow.dispose()
627+
})
628+
})
629+
630+
describe('collapseBelow', () => {
631+
it('should collapse all items when nav width is below collapseBelow (number)', () => {
632+
fixtureEl.innerHTML = [
633+
'<ul class="nav" style="display: flex; width: 400px;" data-bs-toggle="nav-overflow">',
634+
' <li class="nav-item" style="flex: 0 0 50px; width: 50px;"><a class="nav-link" href="#">Link 1</a></li>',
635+
' <li class="nav-item" style="flex: 0 0 50px; width: 50px;"><a class="nav-link" href="#">Link 2</a></li>',
636+
' <li class="nav-item" style="flex: 0 0 50px; width: 50px;"><a class="nav-link" href="#">Link 3</a></li>',
637+
'</ul>'
638+
].join('')
639+
640+
const navEl = fixtureEl.querySelector('[data-bs-toggle="nav-overflow"]')
641+
const navOverflow = new NavOverflow(navEl, {
642+
collapseBelow: 500
643+
})
644+
645+
const hiddenItems = navEl.querySelectorAll('.nav-item[data-bs-nav-overflow="true"]')
646+
expect(hiddenItems.length).toEqual(3)
647+
648+
navOverflow.dispose()
649+
})
650+
651+
it('should not collapse items when nav width is above collapseBelow (number)', () => {
652+
fixtureEl.innerHTML = [
653+
'<ul class="nav" style="display: flex; width: 5000px;" data-bs-toggle="nav-overflow">',
654+
' <li class="nav-item" style="flex: 0 0 50px; width: 50px;"><a class="nav-link" href="#">Link 1</a></li>',
655+
' <li class="nav-item" style="flex: 0 0 50px; width: 50px;"><a class="nav-link" href="#">Link 2</a></li>',
656+
' <li class="nav-item" style="flex: 0 0 50px; width: 50px;"><a class="nav-link" href="#">Link 3</a></li>',
657+
'</ul>'
658+
].join('')
659+
660+
const navEl = fixtureEl.querySelector('[data-bs-toggle="nav-overflow"]')
661+
const navOverflow = new NavOverflow(navEl, {
662+
collapseBelow: 500
663+
})
664+
665+
const hiddenItems = navEl.querySelectorAll('.nav-item[data-bs-nav-overflow="true"]')
666+
expect(hiddenItems.length).toEqual(0)
667+
668+
navOverflow.dispose()
669+
})
670+
671+
it('should resolve a breakpoint string via --bs-breakpoint-{name} CSS variable', () => {
672+
document.documentElement.style.setProperty('--bs-breakpoint-md', '768px')
673+
674+
fixtureEl.innerHTML = [
675+
'<ul class="nav" style="display: flex; width: 400px;" data-bs-toggle="nav-overflow">',
676+
' <li class="nav-item" style="flex: 0 0 50px; width: 50px;"><a class="nav-link" href="#">Link 1</a></li>',
677+
' <li class="nav-item" style="flex: 0 0 50px; width: 50px;"><a class="nav-link" href="#">Link 2</a></li>',
678+
'</ul>'
679+
].join('')
680+
681+
const navEl = fixtureEl.querySelector('[data-bs-toggle="nav-overflow"]')
682+
const navOverflow = new NavOverflow(navEl, {
683+
collapseBelow: 'md'
684+
})
685+
686+
const hiddenItems = navEl.querySelectorAll('.nav-item[data-bs-nav-overflow="true"]')
687+
expect(hiddenItems.length).toEqual(2)
688+
689+
navOverflow.dispose()
690+
document.documentElement.style.removeProperty('--bs-breakpoint-md')
691+
})
692+
693+
it('should respect nav-overflow-keep items when collapsing all', () => {
694+
fixtureEl.innerHTML = [
695+
'<ul class="nav" style="display: flex; width: 400px;" data-bs-toggle="nav-overflow">',
696+
' <li class="nav-item nav-overflow-keep" style="flex: 0 0 50px; width: 50px;"><a class="nav-link" href="#">Keep</a></li>',
697+
' <li class="nav-item" style="flex: 0 0 50px; width: 50px;"><a class="nav-link" href="#">Link 2</a></li>',
698+
' <li class="nav-item" style="flex: 0 0 50px; width: 50px;"><a class="nav-link" href="#">Link 3</a></li>',
699+
'</ul>'
700+
].join('')
701+
702+
const navEl = fixtureEl.querySelector('[data-bs-toggle="nav-overflow"]')
703+
const navOverflow = new NavOverflow(navEl, {
704+
collapseBelow: 500
705+
})
706+
707+
const keepItem = navEl.querySelector('.nav-overflow-keep')
708+
expect(keepItem).not.toHaveClass('d-none')
709+
710+
const hiddenItems = navEl.querySelectorAll('.nav-item[data-bs-nav-overflow="true"]')
711+
expect(hiddenItems.length).toEqual(2)
712+
713+
navOverflow.dispose()
714+
})
715+
716+
it('should be disabled by default (collapseBelow: 0)', () => {
717+
fixtureEl.innerHTML = [
718+
'<ul class="nav" style="display: flex; width: 5000px;" data-bs-toggle="nav-overflow">',
719+
' <li class="nav-item" style="flex: 0 0 50px; width: 50px;"><a class="nav-link" href="#">Link 1</a></li>',
720+
' <li class="nav-item" style="flex: 0 0 50px; width: 50px;"><a class="nav-link" href="#">Link 2</a></li>',
721+
'</ul>'
722+
].join('')
723+
724+
const navEl = fixtureEl.querySelector('[data-bs-toggle="nav-overflow"]')
725+
const navOverflow = new NavOverflow(navEl)
726+
727+
expect(navOverflow._collapseBelow).toEqual(0)
728+
729+
const overflowItem = navEl.querySelector('.nav-overflow-item')
730+
expect(overflowItem).toHaveClass('d-none')
731+
732+
navOverflow.dispose()
733+
})
522734
})
523735

524736
describe('dispose', () => {

scss/_config.scss

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,13 @@ $spacers: (
5353
0: 0,
5454
1: $spacer * .25,
5555
2: $spacer * .5,
56-
3: $spacer,
57-
4: $spacer * 1.5,
58-
5: $spacer * 3,
56+
3: $spacer * .75,
57+
4: $spacer,
58+
5: $spacer * 1.25,
59+
6: $spacer * 1.5,
60+
7: $spacer * 2,
61+
8: $spacer * 2.5,
62+
9: $spacer * 3,
5963
) !default;
6064

6165
$negative-spacers: (

0 commit comments

Comments
 (0)