Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Open `http://localhost:3000/` after startup. The examples are available from
the docs UI and directly at:

- `http://localhost:3000/examples/basic.html`
- `http://localhost:3000/examples/card-header.html`
- `http://localhost:3000/examples/live-search.html`
- `http://localhost:3000/examples/multiple.html`

Expand Down
65 changes: 65 additions & 0 deletions docs/content/examples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ The main examples now live directly on this docs page so they inherit the docs t
- [Search and multi-select workflows](#search-and-multi-select-workflows)
- [Selection text and summaries](#selection-text-and-summaries)
- [Styling and layout](#styling-and-layout)
- [Bootstrap card with header](#bootstrap-card-with-header)
- [Native Bootstrap comparison](#native-bootstrap-comparison)
- [Rich option content](#rich-option-content)
- [Menu behavior](#menu-behavior)
Expand Down Expand Up @@ -546,6 +547,70 @@ The supported values are:

---

### Bootstrap card with header

Use this layout to verify how the picker behaves when it sits directly below a Bootstrap `.card-header`. The menu is appended to `<body>` so it renders outside the card, and auto-dropup is disabled so it does not cover the card header.

<LiveExample
html={String.raw` <div class="card shadow-sm" style="max-width: 42rem;">
<div class="card-header d-flex align-items-center justify-content-between gap-3">
<div>
<strong class="me-2">Question pool</strong>
<span class="text-body-secondary small">Choose which questions are available for this interview.</span>
</div>
<span class="badge text-bg-light border">Draft</span>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Choose candidates</label>
<div class="d-flex gap-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="candidate-scope" id="candidate-all">
<label class="form-check-label" for="candidate-all">All questions</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="candidate-scope" id="candidate-specific" checked>
<label class="form-check-label" for="candidate-specific">Specific questions</label>
</div>
</div>
</div>

<div class="mb-0">
<label class="form-label" for="card-header-example">Select questions</label>
<select
id="card-header-example"
class="selectpicker form-control"
multiple
data-container="body"
data-dropup-auto="false"
data-live-search="true"
data-actions-box="true"
data-selected-text-format="count > 2"
title="Nothing selected">
<option>Accessibility review</option>
<option>API design</option>
<option>Architecture tradeoffs</option>
<option>Background jobs</option>
<option>Bootstrap theming</option>
<option>Database migrations</option>
<option>Deployment readiness</option>
<option>Observability</option>
<option>Performance profiling</option>
<option>Testing strategy</option>
</select>
</div>
</div>
</div>`}
/>

For isolated testing outside the docs page, open the standalone page:

<p>
<Link to="pathname:///examples/card-header.html" target="_blank" rel="noopener noreferrer">
Open the Bootstrap card header example ↗
</Link>
</p>

### Native Bootstrap comparison

`bootstrap-select` is styled to mirror a native Bootstrap&nbsp;5.3 `.form-select`: the
Expand Down
11 changes: 10 additions & 1 deletion docs/content/options.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,15 @@ For security reasons, the `sanitize`, `sanitizeFn`, and `whiteList` options cann
<p>When set to <code>false</code>, the picker follows the width of its container.</p>
</td>
</tr>
<tr>
<td>container</td>
<td>string | element | false</td>
<td><code>false</code></td>
<td>
<p>Appends the dropdown menu to another element, such as <code>'body'</code>, instead of keeping it inside the picker wrapper.</p>
<p>Use this when the picker sits inside constrained UI such as a card or another element with clipping or stacking styles.</p>
</td>
</tr>
<tr>
<td>sanitize</td>
<td>boolean</td>
Expand Down Expand Up @@ -374,7 +383,7 @@ For security reasons, the `sanitize`, `sanitizeFn`, and `whiteList` options cann
/>

:::info Bootstrap 5 runtime defaults
This Bootstrap 5 build no longer supports the legacy `container`, `mobile`, `styleBase`, or `windowPadding` options. When `width` is not set, the picker follows normal Bootstrap sizing and fills its container by default.
This Bootstrap 5 build no longer supports the legacy `mobile`, `styleBase`, or `windowPadding` options. When `width` is not set, the picker follows normal Bootstrap sizing and fills its container by default.
:::

## Tags-style live search and open options
Expand Down
68 changes: 68 additions & 0 deletions docs/static/examples/card-header.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>bootstrap-select card header example</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="../dist/css/bootstrap-select.min.css">
</head>
<body class="p-4">
<main class="container">
<h1>Bootstrap card header example</h1>
<p class="text-body-secondary">Use this page to verify the picker when it is rendered directly below a Bootstrap card header.</p>

<div class="card shadow-sm" style="max-width: 42rem;">
<div class="card-header d-flex align-items-center justify-content-between gap-3">
<div>
<strong class="me-2">Question pool</strong>
<span class="text-body-secondary small">Choose which questions are available for this interview.</span>
</div>
<span class="badge text-bg-light border">Draft</span>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Choose candidates</label>
<div class="d-flex gap-3">
<div class="form-check">
<input class="form-check-input" type="radio" name="candidate-scope" id="candidate-all">
<label class="form-check-label" for="candidate-all">All questions</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="candidate-scope" id="candidate-specific" checked>
<label class="form-check-label" for="candidate-specific">Specific questions</label>
</div>
</div>
</div>

<div class="mb-0">
<label class="form-label" for="card-header-example">Select questions</label>
<select
id="card-header-example"
class="selectpicker form-control"
multiple
data-container="body"
data-dropup-auto="false"
data-live-search="true"
data-actions-box="true"
data-selected-text-format="count > 2"
title="Nothing selected">
<option>Accessibility review</option>
<option>API design</option>
<option>Architecture tradeoffs</option>
<option>Background jobs</option>
<option>Bootstrap theming</option>
<option>Database migrations</option>
<option>Deployment readiness</option>
<option>Observability</option>
<option>Performance profiling</option>
<option>Testing strategy</option>
</select>
</div>
</div>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
<script src="../dist/js/bootstrap-select.min.js"></script>
</body>
</html>
1 change: 1 addition & 0 deletions js/bootstrap-select.api.js
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,7 @@ Selectpicker.DEFAULTS = {
showContent: true,
dropupAuto: true,
header: false,
container: false,
liveSearch: false,
liveSearchPlaceholder: null,
liveSearchNormalize: false,
Expand Down
7 changes: 6 additions & 1 deletion js/bootstrap-select.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ class Selectpicker {
this.clickListener();

var Dropdown = getDropdown();
this.dropdown = new Dropdown(this.button);
this.dropdown = new Dropdown(this.button, this.options.container ? { display: 'static' } : undefined);

// store a reference to the instance for delegated handlers
this.newElement.bootstrapSelectInstance = this;
Expand All @@ -176,6 +176,11 @@ class Selectpicker {

this.setStyle();
this.setWidth();

if (this.options.container) {
this.selectPosition();
}

this._on(this.element, 'hide' + EVENT_KEY, function () {
if (that.isVirtual()) {
// empty menu on close
Expand Down
2 changes: 1 addition & 1 deletion js/bootstrap-select.constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ var changedArguments = null;
// shared flag for spacebar selection handling (mirrors original document data flag)
var spaceSelectFlag = false;

var REMOVED_OPTIONS = ['container', 'display', 'mobile', 'styleBase', 'windowPadding'];
var REMOVED_OPTIONS = ['display', 'mobile', 'styleBase', 'windowPadding'];

function stripRemovedOptions (source) {
if (!source || typeof source !== 'object') return source;
Expand Down
20 changes: 14 additions & 6 deletions js/bootstrap-select.sizing.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,15 @@
if (this.options.dropupAuto) {
// Get the estimated height of the menu without scrollbars.
estimate = liHeight * this.selectpicker.current.data.length + menuPadding.vert;
// Prefer opening downward whenever the menu can still show its controls
// and at least one option below the trigger. Flipping upward too early
// covers preceding form content such as card headers.
var minimumDropdownSpace = liHeight + headerHeight + searchHeight + actionsHeight + doneButtonHeight + menuPadding.vert;
var hasMeaningfulSpaceBelow = this.sizeInfo.selectOffsetBot >= minimumDropdownSpace;

isDropup = this.sizeInfo.selectOffsetTop - this.sizeInfo.selectOffsetBot > this.sizeInfo.menuExtras.vert && estimate + this.sizeInfo.menuExtras.vert + 50 > this.sizeInfo.selectOffsetBot;
isDropup = !hasMeaningfulSpaceBelow &&
this.sizeInfo.selectOffsetTop - this.sizeInfo.selectOffsetBot > this.sizeInfo.menuExtras.vert &&
estimate + this.sizeInfo.menuExtras.vert + 50 > this.sizeInfo.selectOffsetBot;

// ensure dropup doesn't change while searching (so menu doesn't bounce back and forth)
if (this.selectpicker.isSearching === true) {
Expand Down Expand Up @@ -270,15 +277,15 @@
this.bsContainer = createFromHTML('<div class="bs-container" />');

var that = this,
container = resolveContainer(this.options.container),
container = resolveContainer(this.options.container) || document.body,
pos,
containerPos,
actualHeight,
getPlacement = function (element) {
var Dropdown = getDropdown(),
containerPosition = {},
// fall back to dropdown's default display setting if display is not manually set
display = that.options.display || (Dropdown.Default ? Dropdown.Default.display : false);
// relocated menus are positioned by bootstrap-select's container wrapper
display = that.dropdown && that.dropdown._config ? that.dropdown._config.display : (Dropdown.Default ? Dropdown.Default.display : false);

var extraClass = element.getAttribute('class').replace(/form-control|fit-width/gi, '').trim();
if (extraClass) that.bsContainer.classList.add.apply(that.bsContainer.classList, extraClass.split(/\s+/));
Expand All @@ -296,10 +303,12 @@

actualHeight = element.classList.contains(classNames.DROPUP) ? 0 : element.offsetHeight;

// Bootstrap 5 uses Popper for menu positioning
if (display === 'static') {
containerPosition.top = pos.top - containerPos.top + actualHeight;
containerPosition.left = pos.left - containerPos.left;
} else {
containerPosition.top = pos.top - containerPos.top;
containerPosition.left = pos.left - containerPos.left;
}

containerPosition.width = element.offsetWidth;
Expand Down Expand Up @@ -489,4 +498,3 @@
}
}
}

2 changes: 1 addition & 1 deletion sass/bootstrap-select.scss
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,7 @@ select.selectpicker {
border-bottom: 1px solid var(--bs-border-color-translucent, rgba(0, 0, 0, 0.1));

& + .bs-actionsbox {
padding: 0 8px 4px;
padding: 0.5rem 0.75rem 0.25rem;
}

& .form-control {
Expand Down
64 changes: 64 additions & 0 deletions tests/e2e/single-select.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,70 @@ test('menu header renders its close button at the end with compact spacing', asy
});
});

test('dropupAuto keeps the menu below when there is room for controls and one option', async ({ page }) => {
await page.setViewportSize({ width: 900, height: 520 });
await page.goto('/tests/index.html');
await page.waitForFunction(() => window.Selectpicker);

await page.evaluate(() => {
document.body.innerHTML += `
<div class="card shadow-sm" style="max-width: 42rem; margin-top: 3rem;">
<div class="card-header">
<strong>Question pool</strong>
</div>
<div class="card-body">
<label class="form-label" for="card-header-example">Select questions</label>
<select
id="card-header-example"
class="selectpicker form-control"
multiple
data-container="body"
data-live-search="true"
data-actions-box="true"
data-selected-text-format="count > 2"
title="Nothing selected">
<option>Accessibility review</option>
<option>API design</option>
<option>Architecture tradeoffs</option>
<option>Background jobs</option>
<option>Bootstrap theming</option>
<option>Database migrations</option>
<option>Deployment readiness</option>
<option>Observability</option>
<option>Performance profiling</option>
<option>Testing strategy</option>
</select>
</div>
</div>`;

new Selectpicker('#card-header-example');
});

const picker = page.locator('.bootstrap-select').filter({ has: page.locator('[data-id="card-header-example"]') });

await picker.locator('[data-id="card-header-example"]').click();

const bodyContainer = page.locator('body > .bs-container');

await expect(bodyContainer).toHaveCount(1);
await expect(bodyContainer.locator(':scope > .dropdown-menu')).toBeVisible();
await expect(picker).not.toHaveClass(/dropup/);

await expect.poll(async () => page.evaluate(() => {
const buttonRect = document.querySelector('[data-id="card-header-example"]').getBoundingClientRect();
const menuRect = document.querySelector('body > .bs-container .dropdown-menu').getBoundingClientRect();

return menuRect.top >= buttonRect.bottom - 1;
})).toBe(true);

await expect.poll(async () => page.evaluate(() => {
const search = document.querySelector('body > .bs-container .bs-searchbox');
const actionsButton = document.querySelector('body > .bs-container .bs-actionsbox .btn');

return Math.round(actionsButton.getBoundingClientRect().top - search.getBoundingClientRect().bottom);
})).toBeGreaterThan(0);
});

test('native bs.select events are emitted on the original select', async ({ page }) => {
await page.goto('/tests/index.html');
await page.waitForFunction(() => window.Selectpicker);
Expand Down