/** * PXM Select Component * * A flexible, accessible select component that provides dropdown functionality with single * and multiple selection support. Bring your own styling and animation. * This component provides structure and behavior only - all styling is controlled by your CSS. * * Features: * - Single and multiple selection modes * - Keyboard navigation with arrow keys, home/end, enter/space, escape * - Type-ahead search functionality * - Dynamic content support (items can be added/removed after initialization) * - Event-driven animation system (bring your own animation library) * - Accessibility-first design with proper ARIA attributes * - Scroll lock option for modal-like behavior * - Grouping and labeling support * - Custom value display formatting * * Keyboard Navigation: * - `Enter` or `Space` - Open dropdown / Select focused item * - `ArrowDown` - Open dropdown / Focus next item * - `ArrowUp` - Open dropdown / Focus previous item * - `Home` - Focus first item * - `End` - Focus last item * - `Escape` - Close dropdown * - `Tab` - Close dropdown and move to next focusable element * - `A-Z` - Type-ahead search * * Basic Usage: * ```html * * * Choose an option... * * * * * * Apple * * * Banana * * * Cherry * * * * ``` * * With Custom Icon Rotation: * ```html * * * * Choose an option... * * * * * * * ``` * * With Search/Filter Functionality: * ```html * * * Choose an option... * * * * * * * * * * * * Apple * Banana * Cherry * Grape * * * * ``` * * Multiple Selection: * ```html * * * Select multiple... * * * Red * Green * Blue * * * ``` * * Multiple Selection with Custom Separator and Wrapped Values: * ```html * * * * Select colors... * * * Red * Green * * * * * * * Select tags... * * * Tag 1 * Tag 2 * * * * * * * Select options... * * * Option 1 * Option 2 * * * ``` * * With Groups and Labels: * ```html * * * Select a fruit... * * * * * Citrus * Orange * Lemon * * * * * * Berries * Strawberry * Blueberry * * * * ``` * * Dynamic Content: * ```javascript * // Items can be added/removed dynamically * const select = document.querySelector('pxm-select'); * const content = select.querySelector('pxm-select-content'); * * const newItem = document.createElement('pxm-select-item'); * newItem.setAttribute('value', 'grape'); * newItem.innerHTML = 'Grape'; * content.appendChild(newItem); // Automatically initialized * * // Listen for changes * select.addEventListener('pxm:select:items-changed', (e) => { * console.log(`Select now has ${e.detail.itemCount} items`); * }); * * // Get/set values programmatically * select.setValue('apple'); * console.log(select.getValue()); // 'apple' * * select.setValues(['apple', 'banana']); // For multiple selection * console.log(select.getValues()); // ['apple', 'banana'] * * // Interact with individual wrapped values (when wrap-values="true") * const valueElement = select.querySelector('pxm-select-value'); * const valueSpans = valueElement.querySelectorAll('.pxm-select-value-item'); * * valueSpans.forEach(span => { * span.addEventListener('click', (e) => { * e.stopPropagation(); // Prevent select from opening * const value = span.getAttribute('data-value'); * console.log(`Clicked value: ${value}`); * * // Example: Remove value on click * const currentValues = select.getValues(); * const newValues = currentValues.filter(v => v !== value); * select.setValues(newValues); * }); * }); * ``` * * With Animation Library (via events - recommended for CDN): * ```javascript * const select = document.querySelector('pxm-select'); * * select.addEventListener('pxm:select:before-open', (e) => { * const { contentElement } = e.detail; * e.preventDefault(); // Take over the animation * * gsap.fromTo(contentElement, * { opacity: 0, y: -10, scaleY: 0.8 }, * { * opacity: 1, * y: 0, * scaleY: 1, * duration: 0.2, * transformOrigin: 'top', * onComplete: () => { * e.detail.complete(); // Signal animation complete * } * } * ); * }); * * select.addEventListener('pxm:select:before-close', (e) => { * const { contentElement } = e.detail; * e.preventDefault(); // Take over the animation * * gsap.to(contentElement, { * opacity: 0, * y: -10, * scaleY: 0.8, * duration: 0.15, * transformOrigin: 'top', * onComplete: () => { * e.detail.complete(); // Signal animation complete * } * }); * }); * ``` * * With CSS Transitions (default): * ```css * pxm-select-content { * opacity: 0; * transform: translateY(-8px) scaleY(0.9); * transform-origin: top; * transition: opacity 0.2s ease, transform 0.2s ease; * } * * pxm-select-content[data-open="true"] { * opacity: 1; * transform: translateY(0) scaleY(1); * } * ``` * * Consumer Styling Examples: * ```css * /* Style the component structure *\/ * pxm-select { * /* Your layout styles *\/ * position: relative; * display: inline-block; * } * * pxm-select-trigger { * /* Your trigger button styles *\/ * display: flex; * align-items: center; * padding: 8px 12px; * border: 1px solid #ccc; * border-radius: 4px; * cursor: pointer; * } * * pxm-select-content { * /* Your dropdown styles *\/ * position: absolute; * top: 100%; * left: 0; * right: 0; * background: white; * border: 1px solid #ccc; * border-radius: 4px; * box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); * z-index: 1000; * } * * /* ✅ DO: Style based on data attributes (for CSS/JS targeting) *\/ * pxm-select[data-open="true"] pxm-select-trigger { * /* Open state styling *\/ * border-color: #007bff; * } * * pxm-select-item[data-selected="true"] { * /* Selected item styling *\/ * background: #007bff; * color: white; * } * * pxm-select-item[data-focused="true"] { * /* Focused item styling *\/ * background: #f0f0f0; * } * * pxm-select-item[data-disabled="true"] { * /* Disabled item styling *\/ * opacity: 0.5; * cursor: not-allowed; * } * * pxm-select[data-disabled="true"] { * /* Disabled select styling *\/ * opacity: 0.6; * } * * /* Multiple value styling (when wrap-values="true") *\/ * .pxm-select-value-item { * /* Style individual selected values *\/ * background: #e3f2fd; * border-radius: 4px; * padding: 2px 6px; * display: inline-block; * font-size: 0.875em; * } * * .pxm-select-value-item[data-value="important"] { * /* Style specific values by their data-value attribute *\/ * background: #ffebee; * color: #c62828; * } * * /* ❌ DON'T: Style based on ARIA attributes (bad practice) *\/ * pxm-select-item[aria-selected="true"] { * /* Don't use ARIA for styling *\/ * } * ``` * * With Tailwind CSS: * ```html * * * * Choose your option... * * * * * * Apple * * * * * * * ``` * * SSR / Hydration Support: * ```css * /* Recommended: Set initial styles in CSS to prevent hydration flash *\/ * pxm-select-content:not([data-open="true"]) { * display: none; * } * * pxm-select-content[data-open="true"] { * display: block; * } * * /* Optional: Hide content during hydration *\/ * pxm-select:not(:defined) pxm-select-content { * display: none; * } * ``` * * Accessibility: * This component manages both ARIA attributes (for accessibility) and data attributes (for styling/JS). * - ARIA attributes (aria-expanded, aria-selected, etc.) are automatically managed for screen readers * - Data attributes (data-open, data-selected, etc.) are provided for CSS styling and JavaScript hooks * - Additional ARIA attributes, labels, and roles should be added by the consumer as needed * * Events: * - `pxm:select:before-open` - Cancelable. Fired before dropdown opens. * - `pxm:select:after-open` - Fired after dropdown opens. * - `pxm:select:before-close` - Cancelable. Fired before dropdown closes. * - `pxm:select:after-close` - Fired after dropdown closes. * - `pxm:select:before-select` - Cancelable. Fired before item selection. * - `pxm:select:after-select` - Fired after item selection. * - `pxm:select:value-change` - Fired when the selected value(s) change. * - `pxm:select:state-change` - Fired when open/closed state changes. * - `pxm:select:items-changed` - Fired when items are dynamically added/removed. * - `pxm:select:icon-rotate` - Fired when the icon should rotate (open/close states). * - `pxm:select:items-filtered` - Fired when items are filtered by search query. */ import type { SelectConfig, SelectState, SelectEventDetail, SelectItemEventDetail, SelectStateChangeDetail, SelectItemData } from './types'; import { PxmSelectTrigger } from './components/pxm-select-trigger'; import { PxmSelectValue } from './components/pxm-select-value'; import { PxmSelectContent } from './components/pxm-select-content'; import { PxmSelectItem } from './components/pxm-select-item'; import { PxmSelectGroup } from './components/pxm-select-group'; import { PxmSelectLabel } from './components/pxm-select-label'; import { PxmSelectSeparator } from './components/pxm-select-separator'; import { PxmSelectSearch } from './components/pxm-select-search'; declare class PxmSelect extends HTMLElement { private config; private state; private triggerElement?; private valueElement?; private contentElement?; private _items?; private mutationObserver?; private outsideClickListener?; private animationPromises; private hasCustomAnimations; private defaultListeners; private typeAheadString; private typeAheadTimeout?; private get items(); static get observedAttributes(): string[]; constructor(); connectedCallback(): void; disconnectedCallback(): void; attributeChangedCallback(name: string, oldValue: string, newValue: string): void; private setupComponents; private findComponents; private setupEventListeners; private initializeItems; private observeChildChanges; private handleDynamicChanges; private setupDefaultAnimations; private hasEventListener; addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; removeDefaultAnimations(): void; open(): Promise; close(): Promise; toggle(): Promise; selectItem(item: PxmSelectItem): Promise; focusItem(item: PxmSelectItem): void; focusItemByIndex(index: number): void; focusNextItem(): void; focusPreviousItem(): void; focusFirstItem(): void; focusLastItem(): void; focusFirstVisibleItem(): void; focusLastVisibleItem(): void; selectFocusedOrFirstVisibleItem(): void; private selectFirstVisibleItem; private focusInitialItem; filterItems(query: string): void; private handleFilteredFocus; getValue(): string | string[]; getValues(): string[]; setValue(value: string): void; setValues(values: string[]): void; clearSelection(): void; private handleOutsideClick; private createAnimationPromise; private resolveAnimation; private updateAllAttributes; private updateValueDisplay; private dispatchValueChange; private dispatchStateChange; private cleanup; getSelectedItems(): PxmSelectItem[]; getAllItems(): PxmSelectItem[]; isOpen(): boolean; isMultiple(): boolean; /** * Update icon rotation when icon-rotation attribute changes */ private updateIconRotation; /** * Update icon for the select trigger */ private updateIcon; selectFocusedItem(): void; handleTypeAhead(key: string): boolean; private focusFirstTypeAheadMatch; private focusNextTypeAheadMatch; private itemMatchesTypeAhead; clearTypeAhead(): void; } export { PxmSelect }; export { PxmSelectTrigger, PxmSelectValue, PxmSelectContent, PxmSelectItem, PxmSelectGroup, PxmSelectLabel, PxmSelectSeparator, PxmSelectSearch }; export type { SelectEventDetail, SelectItemEventDetail, SelectStateChangeDetail, SelectConfig, SelectState, SelectItemData };