) {
if (changed.has('open')) {
if (this.open && this._toggleEl && this._menuEl) {
this._floating.setOptions({
placement: this._effectivePlacement(),
offset: 2,
flip: true,
shift: true,
});
this._floating.start(this._toggleEl, this._menuEl);
this.dispatchEvent(new CustomEvent('bs-shown', { bubbles: true, composed: true }));
} else {
this._floating.stop();
this.dispatchEvent(new CustomEvent('bs-hidden', { bubbles: true, composed: true }));
}
}
}
/** Show the menu. */
show() {
if (this.open) return;
this.dispatchEvent(new CustomEvent('bs-show', { bubbles: true, composed: true, cancelable: true }));
this.open = true;
}
/** Hide the menu. */
hide() {
if (!this.open) return;
this.dispatchEvent(new CustomEvent('bs-hide', { bubbles: true, composed: true, cancelable: true }));
this.open = false;
}
/** Toggle the menu. */
toggle() {
this.open ? this.hide() : this.show();
}
private _onToggleClick = (ev: Event) => {
ev.stopPropagation();
this.toggle();
};
private _onDocClick = (ev: Event) => {
if (!this.open || !this.autoClose) return;
const path = ev.composedPath();
if (!path.includes(this)) this.hide();
};
private _onKeydown = (ev: KeyboardEvent) => {
if (!this.open) return;
if (ev.key === 'Escape') {
this.hide();
(this._toggleEl as HTMLElement)?.focus();
}
};
private _wrapperClasses() {
// When `split` is set we use `.btn-group` so Bootstrap's split-button
// sibling combinators apply (`.btn-group > .btn + .btn`). Direction
// modifiers (`dropup`, `dropend`, `dropstart`, `dropup-center`) live on
// the wrapper alongside either `.dropdown`, `.btn-group`, or the
// centered variants (`dropdown-center`, `dropup-center`).
const direction = this.drop;
const needsCenter = direction === 'center';
const needsUpCenter = direction === 'up-center';
const base = this.split ? 'btn-group' : needsCenter ? 'dropdown-center' : needsUpCenter ? 'dropup-center' : 'dropdown';
const modifier =
direction === 'up' || direction === 'up-center'
? 'dropup'
: direction === 'end'
? 'dropend'
: direction === 'start'
? 'dropstart'
: '';
return { [base]: true, [modifier]: !!modifier };
}
override render() {
const sizeClass = this.size ? `btn-${this.size}` : '';
// `nav` mode swaps the button-pill classes for the flat nav-link styling
// expected inside `.navbar-nav` and forces an anchor trigger. `split` does
// not combine with `nav` (split-buttons aren't a navbar pattern), so we
// skip the swap when split is set.
const navTrigger = this.nav && !this.split;
const toggleClasses = navTrigger
? classMap({
'nav-link': true,
'dropdown-toggle': !this.noCaret,
})
: classMap({
btn: true,
[`btn-${this.variant}`]: true,
[sizeClass]: !!sizeClass,
'dropdown-toggle': !this.noCaret || this.split,
'dropdown-toggle-split': this.split,
});
const menuClasses = classMap({
'dropdown-menu': true,
'dropdown-menu-end': this.menuEnd,
'dropdown-menu-dark': this.menuDark,
show: this.open,
});
const wrapperClasses = classMap(this._wrapperClasses());
const ariaExpanded = this.open ? 'true' : 'false';
const renderToggle = () => {
if (navTrigger || this.toggleTag === 'a') {
return html`
${this.label}
`;
}
return html``;
};
const renderSplitToggle = () => html`
`;
return html`
${this.split ? renderSplitToggle() : renderToggle()}
`;
}
}
defineElement('bs-dropdown', BsDropdown);
/** `` — single item inside a dropdown. */
export class BsDropdownItem extends BootstrapElement {
@property({ type: String }) href?: string;
@property({ type: Boolean, reflect: true }) disabled = false;
@property({ type: Boolean, reflect: true }) active = false;
@property({ type: Boolean }) divider = false;
@property({ type: Boolean }) header = false;
@property({ type: Boolean }) text = false;
@property({ type: String, attribute: 'as' }) as: 'a' | 'button' = 'a';
private _onClick = (ev: MouseEvent) => {
if (this.disabled) {
ev.preventDefault();
return;
}
// Close parent dropdown unless it has auto-close disabled.
const parent = this.closest('bs-dropdown') as HTMLElement & { autoClose?: boolean; hide?: () => void };
if (parent?.autoClose) parent.hide?.();
};
override render() {
if (this.divider) return html`
`;
if (this.header) return html``;
if (this.text)
return html``;
const classes = classMap({
'dropdown-item': true,
active: this.active,
disabled: this.disabled,
});
if (this.as === 'button') {
return html`
`;
}
return html`
`;
}
}
defineElement('bs-dropdown-item', BsDropdownItem);
// Re-export the menu shell so consumers importing the dropdown module pick
// up the sibling component too.
export { BsDropdownMenu } from './dropdown-menu.js';
import './dropdown-menu.js';
declare global {
interface HTMLElementTagNameMap {
'bs-dropdown': BsDropdown;
'bs-dropdown-item': BsDropdownItem;
}
}