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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# Unreleased

### Changes

- Added the `btn-theme` button style and made it the default `style`. It renders light in light mode and dark in dark mode by following Bootstrap's CSS variables, which is how the default `btn-light` already behaved.
- `btn-light` now looks exactly like `btn-theme` in light mode but stays light in every color mode, including dark mode. Use `btn-theme` (or omit `style`) to follow the color mode.
- A `btn-dark` toggle now has a visible border in dark mode, matching a native dark-mode input.
- The active (selected) menu item is highlighted with a neutral gray instead of the primary color. Override `--bs-dropdown-link-active-bg` and `--bs-dropdown-link-active-color` on `.bootstrap-select .dropdown-menu` to restore the primary highlight.
- The open menu now has Bootstrap's standard drop shadow (`--bs-box-shadow`).

# v1.1.2 (CrestApps fork)

### Fixes
Expand Down
11 changes: 11 additions & 0 deletions docs/content/examples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,13 @@ You can set the button classes via the `data-style` attribute:
<option>Relish</option>
</select>
</div>
<div class="form-group">
<select class="selectpicker" data-style="btn-theme">
<option>Mustard</option>
<option>Ketchup</option>
<option>Relish</option>
</select>
</div>
<div class="form-group">
<select class="selectpicker" data-style="btn-light">
<option>Mustard</option>
Expand Down Expand Up @@ -715,6 +722,10 @@ You can set the button classes via the `data-style` attribute:
...
</select>

<select class="selectpicker" data-style="btn-theme">
...
</select>

<select class="selectpicker" data-style="btn-light">
...
</select>
Expand Down
3 changes: 2 additions & 1 deletion docs/content/options.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -312,10 +312,11 @@ For security reasons, the `sanitize`, `sanitizeFn`, and `whiteList` options cann
<td>style</td>
<td>string | null</td>
<td>
<code>'btn-light'</code>
<code>'btn-theme'</code>
</td>
<td>
<p>When set to a string, add the value to the button's style.</p>
<p><code>btn-theme</code> is provided by bootstrap-select. It renders like a native Bootstrap form control: light in light mode and dark in dark mode, following <code>data-bs-theme</code>. <code>btn-light</code> looks the same as <code>btn-theme</code> in light mode but always stays light, even in dark mode. Other Bootstrap button classes such as <code>btn-dark</code> keep their stock Bootstrap look.</p>
</td>
</tr>
<tr>
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 @@ -34,7 +34,7 @@ var classNames = {
DROPUP: 'dropup',
MENU: 'dropdown-menu',
MENUEND: 'dropdown-menu-end',
BUTTONCLASS: 'btn-light',
BUTTONCLASS: 'btn-theme',
POPOVERHEADER: 'popover-header',
ICONBASE: '',
TICKICON: 'bs-ok-default'
Expand Down
21 changes: 21 additions & 0 deletions js/bootstrap-select.render.js
Original file line number Diff line number Diff line change
Expand Up @@ -438,5 +438,26 @@
if (style) button.classList.remove.apply(button.classList, style.split(' '));
if (buttonClass) button.classList.add.apply(button.classList, buttonClass.split(' '));
}

this.syncMenuTheme();
}

/**
* A `btn-light` button always stays light, so its menu has to stay light too.
* Pin the menu to Bootstrap's light color mode while the button is
* `btn-light`, and release it again when the style changes.
*/
syncMenuTheme () {
var menu = this.menu;

if (!menu || !this.button) return;

if (this.button.classList.contains('btn-light')) {
menu.setAttribute('data-bs-theme', 'light');
menu.setAttribute('data-bs-select-theme-pinned', '');
} else if (menu.hasAttribute('data-bs-select-theme-pinned')) {
menu.removeAttribute('data-bs-theme');
menu.removeAttribute('data-bs-select-theme-pinned');
}
}

79 changes: 66 additions & 13 deletions sass/bootstrap-select.scss
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,13 @@ select.selectpicker {
}
}

// Make the default toggle button look and behave like a native Bootstrap
// form-control. Its colors are driven entirely by Bootstrap's CSS variables
// (with fallbacks) so the control stays visually identical to inputs in both
// light and dark modes, follows the site's --bs-border-color, and never
// darkens on hover/active the way the stock .btn-light does. Consumers can
// restyle it globally by overriding the Bootstrap variables, and choosing a
// different `style` (e.g. btn-primary) opts out of these defaults entirely.
.bootstrap-select > .dropdown-toggle.btn-light {
// `btn-theme` is the default toggle style. It looks and behaves like a native
// Bootstrap form-control: it renders light in light mode and dark in dark mode
// because its colors are driven entirely by Bootstrap's CSS variables (with
// fallbacks). It follows the site's --bs-border-color and never darkens on
// hover/active the way the stock .btn-light does. Consumers can restyle it
// globally by overriding the Bootstrap variables.
.bootstrap-select > .dropdown-toggle.btn-theme {
--bs-btn-color: var(--bs-body-color, #212529);
--bs-btn-bg: var(--bs-body-bg, #fff);
--bs-btn-border-color: var(--bs-border-color, #dee2e6);
Expand All @@ -177,6 +176,42 @@ select.selectpicker {
--bs-btn-focus-shadow-rgb: var(--bs-primary-rgb, 13, 110, 253);
}

// `btn-light` renders exactly like `btn-theme` does in light mode (a white,
// bordered, form-control-like button that does not darken on hover) but stays
// light in dark mode. It uses Bootstrap's fixed palette variables, which do not
// change with `data-bs-theme`, instead of the theme-aware ones above.
.bootstrap-select > .dropdown-toggle.btn-light {
--bs-btn-color: var(--bs-gray-900, #212529);
--bs-btn-bg: var(--bs-white, #fff);
--bs-btn-border-color: var(--bs-gray-300, #dee2e6);
--bs-btn-hover-color: var(--bs-gray-900, #212529);
--bs-btn-hover-bg: var(--bs-white, #fff);
--bs-btn-hover-border-color: var(--bs-gray-300, #dee2e6);
--bs-btn-active-color: var(--bs-gray-900, #212529);
--bs-btn-active-bg: var(--bs-white, #fff);
--bs-btn-active-border-color: var(--bs-gray-300, #dee2e6);
--bs-btn-disabled-color: var(--bs-gray-600, #6c757d);
--bs-btn-disabled-bg: var(--bs-gray-200, #e9ecef);
--bs-btn-disabled-border-color: var(--bs-gray-300, #dee2e6);
--bs-btn-focus-shadow-rgb: var(--bs-primary-rgb, 13, 110, 253);
}

// Deeper menu shadow on a dark page, where the default one is not visible. A
// menu pinned to the light color mode (a `btn-light` toggle) keeps the light one.
[data-bs-theme="dark"] .bootstrap-select .dropdown-menu:not(.inner):not([data-bs-theme="light"]) {
--bs-select-menu-shadow: 0 0.5rem 1.25rem rgba(var(--bs-black-rgb, 0, 0, 0), 0.65);
}

// In dark mode a `btn-dark` toggle would otherwise blend into the page because
// its border is the same color as its background. Give it the same visible
// border a native dark-mode input has.
[data-bs-theme="dark"] .bootstrap-select > .dropdown-toggle.btn-dark {
--bs-btn-border-color: var(--bs-border-color, #495057);
--bs-btn-hover-border-color: var(--bs-border-color, #495057);
--bs-btn-active-border-color: var(--bs-border-color, #495057);
--bs-btn-disabled-border-color: var(--bs-border-color, #495057);
}

// The selectpicker components
.bootstrap-select {
&.form-control {
Expand Down Expand Up @@ -341,6 +376,23 @@ select.selectpicker {
min-width: 100%;
box-sizing: border-box;

// Bootstrap highlights the active item with the primary color. Use the same
// neutral gray a native select highlights with instead, driven by Bootstrap
// variables so it works in light and dark modes. Consumers can override
// these variables to restore the primary highlight.
--bs-dropdown-link-hover-color: var(--bs-body-color, #212529);
--bs-dropdown-link-hover-bg: var(--bs-tertiary-bg, #f8f9fa);
--bs-dropdown-link-active-color: var(--bs-body-color, #212529);
--bs-dropdown-link-active-bg: var(--bs-secondary-bg, #e9ecef);

// The popup floats over the page, so lift it with a drop shadow. The inner
// scroll container is excluded (see `.inner` below). Bootstrap's own shadow
// is a faint black that vanishes on a dark page, so dark mode gets a deeper
// one. Override `--bs-select-menu-shadow` to change it.
&:not(.inner) {
box-shadow: var(--bs-select-menu-shadow, var(--bs-box-shadow, 0 0.5rem 1rem rgba(var(--bs-black-rgb, 0, 0, 0), 0.15)));
}

> .inner:focus {
outline: none !important;
}
Expand All @@ -359,7 +411,7 @@ select.selectpicker {
position: relative;

&.active small {
color: $input-alt-color-placeholder !important;
color: var(--bs-secondary-color, #6c757d) !important;
}

&.disabled a {
Expand Down Expand Up @@ -400,7 +452,7 @@ select.selectpicker {
background: var(--bs-tertiary-bg, rgb(245, 245, 245));
color: var(--bs-secondary-color, inherit);
border: 1px solid var(--bs-border-color, rgb(227, 227, 227));
box-shadow: inset 0 1px 1px fade(rgb(0, 0, 0), 5%);
box-shadow: inset 0 1px 1px rgba(var(--bs-black-rgb, 0, 0, 0), 0.05);
pointer-events: none;
opacity: 0.9;
box-sizing: border-box;
Expand Down Expand Up @@ -557,7 +609,7 @@ select.selectpicker {
content: '';
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid white;
border-bottom: 6px solid var(--bs-dropdown-bg, var(--bs-body-bg, #fff));
position: absolute;
bottom: -4px;
left: 10px;
Expand All @@ -576,7 +628,7 @@ select.selectpicker {
&:after {
bottom: auto;
top: -4px;
border-top: 6px solid white;
border-top: 6px solid var(--bs-dropdown-bg, var(--bs-body-bg, #fff));
border-bottom: 0;
}
}
Expand Down Expand Up @@ -660,6 +712,7 @@ select.selectpicker {

// Drive the Select All / Deselect All buttons from Bootstrap variables so
// they stay legible in dark mode instead of remaining light-on-light.
& .btn-theme,
& .btn-light {
--bs-btn-color: var(--bs-body-color, #212529);
--bs-btn-bg: var(--bs-tertiary-bg, #f8f9fa);
Expand Down Expand Up @@ -750,7 +803,7 @@ select.selectpicker {
font-size: 0.8125rem;
line-height: 1.125rem;
text-align: left;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
box-shadow: 0 1px 2px rgba(var(--bs-black-rgb, 0, 0, 0), 0.04);
transition: background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out;
appearance: none;
-webkit-appearance: none;
Expand Down
4 changes: 2 additions & 2 deletions sass/variables.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
$color-red-error: rgb(185, 74, 72) !default;
$color-green-success: #28a745 !default;
$color-grey-arrow: rgba(204, 204, 204, 0.2) !default;
$color-grey-arrow: var(--bs-border-color-translucent, rgba(204, 204, 204, 0.2)) !default;

$width-default: 100% !default;

Expand All @@ -11,7 +11,7 @@ $zindex-select-dropdown: 1060 !default; // must be higher than a modal backgroun
// (`.form-control`/`.form-select` placeholders resolve to `--bs-secondary-color`)
// so the selectpicker's placeholder matches native inputs in light and dark modes.
$input-color-placeholder: var(--bs-secondary-color, #6c757d) !default;
$input-alt-color-placeholder: rgba(255, 255, 255, 0.5) !default;
$input-alt-color-placeholder: rgba(var(--bs-white-rgb, 255, 255, 255), 0.5) !default;

//** Bootstrap 5.3-style chevron, checkmark, and remove (×) icons.
// Rendered through CSS masks so they inherit the current text color and share a
Expand Down
118 changes: 118 additions & 0 deletions tests/e2e/theme-styles.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
const { test, expect } = require('@playwright/test');

// Renders a selectpicker with the given style under the given color mode and
// returns the computed styles that the theming rules are responsible for.
async function render (page, { theme, style }) {
return page.evaluate(({ theme, style }) => {
document.documentElement.setAttribute('data-bs-theme', theme);

const select = document.createElement('select');
select.className = 'selectpicker';
if (style) { select.setAttribute('data-style', style); }
['One', 'Two', 'Three'].forEach((text) => {
const option = document.createElement('option');
option.textContent = text;
select.appendChild(option);
});
select.options[1].selected = true;
document.body.appendChild(select);

window.Selectpicker.getOrCreateInstance(select);
const wrapper = select.closest('.bootstrap-select') || select.parentElement;
const button = wrapper.querySelector('.dropdown-toggle');
button.click();

const menu = wrapper.querySelector('.dropdown-menu:not(.inner)');
const active = wrapper.querySelector('.dropdown-menu li.active a, .dropdown-menu li.selected a');
const css = (el) => (el ? getComputedStyle(el) : null);

return {
buttonClass: button.className,
buttonBg: css(button).backgroundColor,
buttonBorder: css(button).borderTopColor,
buttonColor: css(button).color,
bodyBg: css(document.body).backgroundColor,
borderColor: css(document.documentElement).getPropertyValue('--bs-border-color').trim(),
menuBg: css(menu).backgroundColor,
menuColor: css(menu).color,
menuShadow: css(menu).boxShadow,
activeBg: active ? css(active).backgroundColor : null,
primary: css(document.documentElement).getPropertyValue('--bs-primary').trim()
};
}, { theme, style });
}

test.describe('Button styles and menu theming', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/tests/index.html');
await page.waitForFunction(() => window.Selectpicker);
});

test('default style is btn-theme', async ({ page }) => {
const result = await render(page, { theme: 'light' });
expect(result.buttonClass).toContain('btn-theme');
expect(result.buttonClass).not.toContain('btn-light');
});

test('btn-theme follows the color mode', async ({ page }) => {
const light = await render(page, { theme: 'light', style: 'btn-theme' });
expect(light.buttonBg).toBe(light.bodyBg);
await page.reload();
await page.waitForFunction(() => window.Selectpicker);
const dark = await render(page, { theme: 'dark', style: 'btn-theme' });
expect(dark.buttonBg).toBe(dark.bodyBg);
expect(dark.buttonBg).not.toBe(light.buttonBg);
});

test('btn-light stays light in dark mode', async ({ page }) => {
const dark = await render(page, { theme: 'dark', style: 'btn-light' });
expect(dark.buttonBg).toBe('rgb(255, 255, 255)');
expect(dark.buttonBg).not.toBe(dark.bodyBg);
});

test('btn-light menu stays light in dark mode', async ({ page }) => {
const dark = await render(page, { theme: 'dark', style: 'btn-light' });
expect(dark.menuBg).toBe('rgb(255, 255, 255)');
expect(dark.activeBg).toBe('rgb(233, 236, 239)');
});

test('btn-theme menu follows dark mode', async ({ page }) => {
const dark = await render(page, { theme: 'dark', style: 'btn-theme' });
expect(dark.menuBg).not.toBe('rgb(255, 255, 255)');
});

test('btn-light looks exactly like btn-theme in light mode', async ({ page }) => {
const theme = await render(page, { theme: 'light', style: 'btn-theme' });
await page.reload();
await page.waitForFunction(() => window.Selectpicker);
const light = await render(page, { theme: 'light', style: 'btn-light' });
expect(light.buttonBg).toBe(theme.buttonBg);
expect(light.buttonBorder).toBe(theme.buttonBorder);
expect(light.buttonColor).toBe(theme.buttonColor);
});

test('btn-dark has a visible border in dark mode', async ({ page }) => {
const dark = await render(page, { theme: 'dark', style: 'btn-dark' });
expect(dark.buttonBorder).not.toBe(dark.buttonBg);
});

test('open menu has a drop shadow', async ({ page }) => {
const result = await render(page, { theme: 'light' });
expect(result.menuShadow).not.toBe('none');
});

test('dark mode menu shadow is deeper than the light one', async ({ page }) => {
const light = await render(page, { theme: 'light' });
await page.reload();
await page.waitForFunction(() => window.Selectpicker);
const dark = await render(page, { theme: 'dark' });
expect(dark.menuShadow).not.toBe('none');
expect(dark.menuShadow).not.toBe(light.menuShadow);
expect(dark.menuShadow).toContain('0.65');
});

test('active item is gray, not the primary color', async ({ page }) => {
const result = await render(page, { theme: 'light' });
expect(result.activeBg).toBe('rgb(233, 236, 239)');
});
});
Loading