import {type Placement, autoUpdate, computePosition, flip, offset, shift, size} from '@floating-ui/dom' import DetailsMenuElement from '@github/details-menu-element' import {controllerFactory} from '@utils/createController' import {useClickOutside, useMutation} from 'stimulus-use' export default class ComboboxController extends controllerFactory()({ targets: { wrapper: HTMLDetailsElement, button: HTMLElement, popover: DetailsMenuElement, options: HTMLDivElement, }, values: { clamped: Boolean, placement: String, dynamicLabelPrefix: String, }, }) { private changedIds = new Set() unsubAutoUpdate: (() => void) | undefined clickOutside() { this.element.open = false this.setupAutoUpdate() this.close() } close() { if (this.changedIds.size > 0) { this.dispatch('changed') this.changedIds.clear() } } connect() { useClickOutside(this) useMutation(this, {childList: true, subtree: true}) this.setupAutoUpdate() this.setupForm() } disconnect() { this.unsubAutoUpdate?.() } setupForm(): void { for (const formType of ['checkbox', 'radio']) { // https://github.com/github/details-menu-element?tab=readme-ov-file#markup for (const el of this.popoverTarget.querySelectorAll(`input[type="${formType}"]`)) { el.addEventListener('change', (e: {target: HTMLInputElement}) => { const value = e.target as HTMLInputElement if (this.changedIds.has(value)) { this.changedIds.delete(value) } else { this.changedIds.add(value) } this.dispatch('clicked', {detail: value}) }) } } } setupAutoUpdate(): void { if (!this.element.open) { this.unsubAutoUpdate?.() return } const updatePopoverPosition = (): void => { const options = this.optionsTarget const shouldClamp = this.clampedValue void computePosition(this.buttonTarget, this.popoverTarget, { middleware: [ offset(6), flip(), shift({padding: 6}), size({ apply({availableHeight}) { let maxHeight = availableHeight - 6 if (shouldClamp && maxHeight > 400) maxHeight = 400 Object.assign(options.style, { maxHeight: `${maxHeight}px`, }) }, }), ], placement: this.placementValue as Placement, strategy: 'fixed', }).then(({x, y}) => { Object.assign(this.popoverTarget.style, { left: `${x}px`, top: `${y}px`, }) }) } updatePopoverPosition() this.unsubAutoUpdate = autoUpdate(this.buttonTarget, this.popoverTarget, updatePopoverPosition) } updateButtonLabel(e: Event): void { const target = e.target as HTMLDetailsElement const checkedRadioButton = target.querySelector('input[type=radio]:checked') as HTMLInputElement | null if (!checkedRadioButton) return const selectedText = (checkedRadioButton.labels?.item(0) as HTMLLabelElement).textContent this.buttonTarget.querySelector( '[data-ariadne-ui-button-target="content"]', ).textContent = `${this.dynamicLabelPrefixValue}${selectedText}` } toggle(): void { this.element.open = !this.element.open this.setupAutoUpdate() } }