Skip to content

Commit

Permalink
fix: virtual scroll breaks on last chunk
Browse files Browse the repository at this point in the history
  • Loading branch information
nick-lai committed Nov 21, 2023
1 parent d2577c0 commit 52031eb
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 14 deletions.
107 changes: 107 additions & 0 deletions cypress/e2e/virtual-scroll.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
const testArray = new Array(1000).fill(0).map((x, i) => ({
text: `Option ${i + 1}`
}));

const selectpickerConfigs = [{
title: 'Select option with line-height style',
config: {
html: `
<select class="selectpicker" data-virtual-scroll="true">
${testArray.map(({ text }) =>
`<option value="${text}" style="line-height: 1.6275;">${text}</option>`
).join('')}
</select>
`,
options: {
virtualScroll: true
}
}
}, {
title: 'Select option with default style',
config: {
html: `
<select class="selectpicker" data-virtual-scroll="true">
${testArray.map(({ text }) =>
`<option value="${text}">${text}</option>`
).join('')}
</select>
`,
options: {
virtualScroll: true
}
}
}];

describe('Virtual scroll with custom select option style', () => {
beforeEach(() => {
cy.visit('/tests/index.html');
});

selectpickerConfigs.forEach(({title, config}) => {
it(title, () => {
cy.selectpicker(config).then(($select) => {
const firstSelectOptionText = testArray.at(0).text;
const lastSelectOptionText = testArray.at(-1).text;
const button = `[data-id="${$select[0].id}"]`;

cy.get(button).click();
const dropdownMenu = cy.get('.dropdown-menu [role="listbox"]');

dropdownMenu.type('{upArrow}');
cy.get('li').contains(lastSelectOptionText)
.should('have.class', 'active');

dropdownMenu.type('{enter}');
cy.get(button).find('.filter-option-inner-inner')
.should('contain', lastSelectOptionText);

cy.get(button).click();
dropdownMenu.type('{downArrow}');

cy.get('li').contains(firstSelectOptionText)
.should('have.class', 'active');

dropdownMenu.type('{enter}');
cy.get(button).find('.filter-option-inner-inner')
.should('contain', firstSelectOptionText);

const n = 20;
const typeOptions = {
// Delay after each keypress (Default: 10)
delay: 0,
};

// Test the first n options.
cy.get(button).click();

for (let i = 1, iterator = testArray.entries(); i <= n; i++) {
const [, { text }] = iterator.next().value;

// Sampling (testing every 5 typing actions)
if (i % 5 === 0) {
cy.get('li').contains(text)
.should('have.class', 'active');
}
dropdownMenu.type('{downArrow}', typeOptions);
}
cy.get(button).click();

// Test the last n select options.
const reversedArray = Array.from(testArray).reverse();
cy.get(button).click();

for (let i = 1, iterator = reversedArray.entries(); i <= n; i++) {
const [, { text }] = iterator.next().value;
dropdownMenu.type('{upArrow}', typeOptions);

// Sampling (testing every 5 typing actions)
if (i % 5 === 0) {
cy.get('li').contains(text)
.should('have.class', 'active');
}
}
cy.get(button).click();
});
});
});
});
62 changes: 48 additions & 14 deletions js/bootstrap-select.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,40 @@
return attributesObject;
}

/**
* Check if element.getBoundingClientRect() is supported.
*
* @param {HTMLElement} element
* @returns {boolean}
*/
function isGetBoundingClientRectSupported (element) {
return typeof element.getBoundingClientRect === 'function';
}

/**
* Attempt to get rendered height of the element.
*
* @param {HTMLElement} element
* @returns {number}
*/
function attemptGetRenderedHeight (element) {
return isGetBoundingClientRectSupported(element)
? element.getBoundingClientRect().height
: element.offsetHeight;
}

/**
* Attempt to get rendered width of the element.
*
* @param {HTMLElement} element
* @returns {number}
*/
function attemptGetRenderedWidth (element) {
return isGetBoundingClientRectSupported(element)
? element.getBoundingClientRect().width
: element.offsetWidth;
}

// Polyfill for browsers with no classList support
// Remove in v2
if (!('classList' in document.createElement('_'))) {
Expand Down Expand Up @@ -1458,15 +1492,15 @@
// if an option is encountered that is wider than the current menu width, update the menu width accordingly
// switch to ResizeObserver with increased browser support
if (isVirtual === true && that.sizeInfo.hasScrollBar) {
var menuInnerInnerWidth = menuInner.firstChild.offsetWidth;
var menuInnerInnerWidth = attemptGetRenderedWidth(menuInner.firstChild);

if (init && menuInnerInnerWidth < that.sizeInfo.menuInnerInnerWidth && that.sizeInfo.totalMenuWidth > that.sizeInfo.selectWidth) {
menuInner.firstChild.style.minWidth = that.sizeInfo.menuInnerInnerWidth + 'px';
} else if (menuInnerInnerWidth > that.sizeInfo.menuInnerInnerWidth) {
// set to 0 to get actual width of menu
that.$menu[0].style.minWidth = 0;

var actualMenuWidth = menuInner.firstChild.offsetWidth;
var actualMenuWidth = attemptGetRenderedWidth(menuInner.firstChild);

if (actualMenuWidth > that.sizeInfo.menuInnerInnerWidth) {
that.sizeInfo.menuInnerInnerWidth = actualMenuWidth;
Expand Down Expand Up @@ -2115,7 +2149,7 @@
doneButton = this.options.doneButton && this.multiple && this.$menu.find('.bs-donebutton').length > 0 ? this.$menu.find('.bs-donebutton')[0].cloneNode(true) : null,
firstOption = this.$element[0].options[0];

this.sizeInfo.selectWidth = this.$newElement[0].offsetWidth;
this.sizeInfo.selectWidth = attemptGetRenderedWidth(this.$newElement[0]);

text.className = 'text';
a.className = 'dropdown-item ' + (firstOption ? firstOption.className : '');
Expand Down Expand Up @@ -2169,15 +2203,15 @@

document.body.appendChild(newElement);

var liHeight = li.offsetHeight,
dropdownHeaderHeight = dropdownHeader ? dropdownHeader.offsetHeight : 0,
headerHeight = header ? header.offsetHeight : 0,
searchHeight = search ? search.offsetHeight : 0,
actionsHeight = actions ? actions.offsetHeight : 0,
doneButtonHeight = doneButton ? doneButton.offsetHeight : 0,
var liHeight = attemptGetRenderedHeight(li),
dropdownHeaderHeight = dropdownHeader ? attemptGetRenderedHeight(dropdownHeader) : 0,
headerHeight = header ? attemptGetRenderedHeight(header) : 0,
searchHeight = search ? attemptGetRenderedHeight(search) : 0,
actionsHeight = actions ? attemptGetRenderedHeight(actions) : 0,
doneButtonHeight = doneButton ? attemptGetRenderedHeight(doneButton) : 0,
dividerHeight = $(divider).outerHeight(true),
menuStyle = window.getComputedStyle(menu),
menuWidth = menu.offsetWidth,
menuWidth = attemptGetRenderedWidth(menu),
menuPadding = {
vert: toInteger(menuStyle.paddingTop) +
toInteger(menuStyle.paddingBottom) +
Expand All @@ -2200,7 +2234,7 @@

menuInner.style.overflowY = 'scroll';

scrollBarWidth = menu.offsetWidth - menuWidth;
scrollBarWidth = attemptGetRenderedWidth(menu) - menuWidth;

document.body.removeChild(newElement);

Expand All @@ -2217,7 +2251,7 @@
this.sizeInfo.menuInnerInnerWidth = menuWidth - menuPadding.horiz;
this.sizeInfo.totalMenuWidth = this.sizeInfo.menuWidth;
this.sizeInfo.scrollBarWidth = scrollBarWidth;
this.sizeInfo.selectHeight = this.$newElement[0].offsetHeight;
this.sizeInfo.selectHeight = attemptGetRenderedHeight(this.$newElement[0]);

this.setPositionData();
},
Expand Down Expand Up @@ -2438,15 +2472,15 @@
containerPos = { top: 0, left: 0 };
}

actualHeight = $element.hasClass(classNames.DROPUP) ? 0 : $element[0].offsetHeight;
actualHeight = $element.hasClass(classNames.DROPUP) ? 0 : attemptGetRenderedHeight($element[0]);

// Bootstrap 4+ uses Popper for menu positioning
if (version.major < 4 || display === 'static') {
containerPosition.top = pos.top - containerPos.top + actualHeight;
containerPosition.left = pos.left - containerPos.left;
}

containerPosition.width = $element[0].offsetWidth;
containerPosition.width = attemptGetRenderedWidth($element[0]);

that.$bsContainer.css(containerPosition);
};
Expand Down

0 comments on commit 52031eb

Please sign in to comment.