diff --git a/packages/webawesome/docs/docs/components/dropdown.md b/packages/webawesome/docs/docs/components/dropdown.md deleted file mode 100644 index 647cfd36d..000000000 --- a/packages/webawesome/docs/docs/components/dropdown.md +++ /dev/null @@ -1,182 +0,0 @@ ---- -title: Dropdown -description: 'Dropdowns expose additional content that "drops down" in a panel.' -tags: [actions, apps] -icon: dropdown ---- - -Dropdowns consist of a trigger and a panel. By default, activating the trigger will expose the panel and interacting outside of the panel will close it. - -Dropdowns are designed to work well with [menus](/docs/components/menu) to provide a list of options the user can select from. However, dropdowns can also be used in lower-level applications (e.g. [color picker](/docs/components/color-picker)). The API gives you complete control over showing, hiding, and positioning the panel. - -```html {.example} - - Dropdown - - Dropdown Item 1 - Dropdown Item 2 - Dropdown Item 3 - - Checkbox - Disabled - - - Prefix - - - - Suffix Icon - - - - -``` - -## Examples - -### Getting the Selected Item - -When dropdowns are used with [menus](/docs/components/menu), you can listen for the [`wa-select`](/docs/components/menu#events) event to determine which menu item was selected. The menu item element will be exposed in `event.detail.item`. You can set `value` props to make it easier to identify commands. - -```html {.example} - - - -``` - -Alternatively, you can listen for the `click` event on individual menu items. Note that, using this approach, disabled menu items will still emit a `click` event. - -```html {.example} - - - -``` - -### Placement - -The preferred placement of the dropdown can be set with the `placement` attribute. Note that the actual position may vary to ensure the panel remains in the viewport. - -```html {.example} - - Edit - - Cut - Copy - Paste - - Find - Replace - - -``` - -### Distance - -The distance from the panel to the trigger can be customized using the `distance` attribute. This value is specified in pixels. - -```html {.example} - - Edit - - Cut - Copy - Paste - - Find - Replace - - -``` - -### Skidding - -The offset of the panel along the trigger can be customized using the `skidding` attribute. This value is specified in pixels. - -```html {.example} - - Edit - - Cut - Copy - Paste - - Find - Replace - - -``` - -### Submenus - -To create a submenu, nest an `` element in a [menu item](/docs/components/menu-item). - -```html {.example} - - Edit - - - Undo - Redo - - Cut - Copy - Paste - - - Find - - Find… - Find Next - Find Previous - - - - Transformations - - Make uppercase - Make lowercase - Capitalize - - - - -``` - -:::warning -As a UX best practice, avoid using more than one level of submenu when possible. -::: diff --git a/packages/webawesome/src/components/dropdown/dropdown.css b/packages/webawesome/src/components/dropdown/dropdown.css deleted file mode 100644 index ef8472162..000000000 --- a/packages/webawesome/src/components/dropdown/dropdown.css +++ /dev/null @@ -1,60 +0,0 @@ -:host { - --box-shadow: var(--wa-shadow-m); - - display: inline-block; -} - -.dropdown::part(popup) { - z-index: 900; -} - -.dropdown[data-current-placement^='top']::part(popup) { - transform-origin: bottom; -} - -.dropdown[data-current-placement^='bottom']::part(popup) { - transform-origin: top; -} - -.dropdown[data-current-placement^='left']::part(popup) { - transform-origin: right; -} - -.dropdown[data-current-placement^='right']::part(popup) { - transform-origin: left; -} - -#trigger { - display: block; /* for boundingClientRect */ -} - -.panel { - font: inherit; - box-shadow: var(--box-shadow); - border-radius: var(--wa-border-radius-m); - pointer-events: none; -} - -.dropdown-open .panel { - display: block; - pointer-events: all; -} - -/* Sizes */ -:host([size='small']) ::slotted(wa-menu) { - font-size: var(--wa-font-size-s); -} - -:host([size='medium']) ::slotted(wa-menu) { - font-size: var(--wa-font-size-m); -} - -:host([size='large']) ::slotted(wa-menu) { - font-size: var(--wa-font-size-l); -} - -/* When users slot a menu, make sure it conforms to the popup's auto-size */ -::slotted(wa-menu) { - max-width: var(--auto-size-available-width) !important; - max-height: var(--auto-size-available-height) !important; -} diff --git a/packages/webawesome/src/components/dropdown/dropdown.ts b/packages/webawesome/src/components/dropdown/dropdown.ts deleted file mode 100644 index 9fd7582b3..000000000 --- a/packages/webawesome/src/components/dropdown/dropdown.ts +++ /dev/null @@ -1,430 +0,0 @@ -import { html } from 'lit'; -import { customElement, property, query } from 'lit/decorators.js'; -import { classMap } from 'lit/directives/class-map.js'; -import { ifDefined } from 'lit/directives/if-defined.js'; -import { WaAfterHideEvent } from '../../events/after-hide.js'; -import { WaAfterShowEvent } from '../../events/after-show.js'; -import { WaHideEvent } from '../../events/hide.js'; -import { WaShowEvent } from '../../events/show.js'; -import { animateWithClass } from '../../internal/animate.js'; -import { waitForEvent } from '../../internal/event.js'; -import { watch } from '../../internal/watch.js'; -import WebAwesomeElement from '../../internal/webawesome-element.js'; -import sizeStyles from '../../styles/utilities/size.css'; -import type WaButton from '../button/button.js'; -import '../popup/popup.js'; -import type WaPopup from '../popup/popup.js'; -import styles from './dropdown.css'; - -/** - * @summary Dropdowns expose additional content that "drops down" in a panel. - * @documentation https://backers.webawesome.com/docs/components/dropdown - * @status stable - * @since 2.0 - * - * @dependency wa-popup - * - * @slot - The dropdown's main content. - * @slot trigger - The dropdown's trigger, usually a `` element. - * - * @event wa-show - Emitted when the dropdown opens. - * @event wa-after-show - Emitted after the dropdown opens and all animations are complete. - * @event wa-hide - Emitted when the dropdown closes. - * @event wa-after-hide - Emitted after the dropdown closes and all animations are complete. - * - * @cssproperty --box-shadow - The shadow effects around the dropdown's edges. - * - * @csspart base - The component's base wrapper, a `` element. - * @csspart base__popup - The popup's exported `popup` part. Use this to target the tooltip's popup container. - * @csspart trigger - The container that wraps the trigger. - * @csspart panel - The panel that gets shown when the dropdown is open. - */ -@customElement('wa-dropdown') -export default class WaDropdown extends WebAwesomeElement { - static css = [sizeStyles, styles]; - - @query('.dropdown') popup: WaPopup; - @query('#trigger') trigger: HTMLSlotElement; - @query('.panel') panel: HTMLSlotElement; - - /** - * Indicates whether or not the dropdown is open. You can toggle this attribute to show and hide the dropdown, or you - * can use the `show()` and `hide()` methods and this attribute will reflect the dropdown's open state. - */ - @property({ type: Boolean, reflect: true }) open = false; - - /** - * The preferred placement of the dropdown panel. Note that the actual placement may vary as needed to keep the panel - * inside of the viewport. - */ - @property({ reflect: true }) placement: - | 'top' - | 'top-start' - | 'top-end' - | 'bottom' - | 'bottom-start' - | 'bottom-end' - | 'right' - | 'right-start' - | 'right-end' - | 'left' - | 'left-start' - | 'left-end' = 'bottom-start'; - - /** Disables the dropdown so the panel will not open. */ - @property({ type: Boolean, reflect: true }) disabled = false; - - /** - * By default, the dropdown is closed when an item is selected. This attribute will keep it open instead. Useful for - * dropdowns that allow for multiple interactions. - */ - @property({ attribute: 'stay-open-on-select', type: Boolean, reflect: true }) stayOpenOnSelect = false; - - /** - * The dropdown will close when the user interacts outside of this element (e.g. clicking). Useful for composing other - * components that use a dropdown internally. - */ - @property({ attribute: false }) containingElement?: HTMLElement; - - /** The distance in pixels from which to offset the panel away from its trigger. */ - @property({ type: Number }) distance = 0; - - /** The distance in pixels from which to offset the panel along its trigger. */ - @property({ type: Number }) skidding = 0; - - /** - * Syncs the popup width or height to that of the trigger element. - */ - @property({ reflect: true }) sync: 'width' | 'height' | 'both' | undefined = undefined; - - connectedCallback() { - super.connectedCallback(); - - if (!this.containingElement) { - this.containingElement = this; - } - } - - firstUpdated() { - this.panel.hidden = !this.open; - - // If the dropdown is visible on init, update its position - if (this.open) { - this.addOpenListeners(); - this.popup.active = true; - } - } - - disconnectedCallback() { - super.disconnectedCallback(); - this.removeOpenListeners(); - this.hide(); - } - - focusOnTrigger() { - const trigger = this.trigger.assignedElements({ flatten: true })[0] as HTMLElement | undefined; - if (typeof trigger?.focus === 'function') { - trigger.focus(); - } - } - - getMenu() { - return this.panel.assignedElements({ flatten: true }).find(el => el.tagName.toLowerCase() === 'wa-menu') as - | any - | undefined; - } - - private handleKeyDown = (event: KeyboardEvent) => { - // Close when escape is pressed inside an open dropdown. We need to listen on the panel itself and stop propagation - // in case any ancestors are also listening for this key. - if (this.open && event.key === 'Escape') { - event.stopPropagation(); - this.hide(); - this.focusOnTrigger(); - } - }; - - private handleDocumentKeyDown = (event: KeyboardEvent) => { - // Close when escape or tab is pressed - if (event.key === 'Escape' && this.open) { - event.stopPropagation(); - this.focusOnTrigger(); - this.hide(); - return; - } - - // Handle tabbing - if (event.key === 'Tab') { - // Tabbing within an open menu should close the dropdown and refocus the trigger - if (this.open && document.activeElement?.tagName.toLowerCase() === 'wa-menu-item') { - event.preventDefault(); - this.hide(); - this.focusOnTrigger(); - return; - } - - // Tabbing outside of the containing element closes the panel - // - // If the dropdown is used within a shadow DOM, we need to obtain the activeElement within that shadowRoot, - // otherwise `document.activeElement` will only return the name of the parent shadow DOM element. - setTimeout(() => { - const activeElement = - this.containingElement?.getRootNode() instanceof ShadowRoot - ? document.activeElement?.shadowRoot?.activeElement - : document.activeElement; - - if ( - !this.containingElement || - activeElement?.closest(this.containingElement.tagName.toLowerCase()) !== this.containingElement - ) { - this.hide(); - } - }); - } - }; - - private handleDocumentMouseDown = (event: MouseEvent) => { - // Close when clicking outside of the containing element - const path = event.composedPath(); - if (this.containingElement && !path.includes(this.containingElement)) { - this.hide(); - } - }; - - private handlePanelSelect = (event: CustomEvent) => { - const target = event.target as HTMLElement; - - // Hide the dropdown when a menu item is selected - if (!this.stayOpenOnSelect && target.tagName.toLowerCase() === 'wa-menu') { - this.hide(); - this.focusOnTrigger(); - } - }; - - handleTriggerClick() { - if (this.open) { - this.hide(); - } else { - this.show(); - this.focusOnTrigger(); - } - } - - async handleTriggerKeyDown(event: KeyboardEvent) { - // When spacebar/enter is pressed, show the panel but don't focus on the menu. This let's the user press the same - // key again to hide the menu in case they don't want to make a selection. - if ([' ', 'Enter'].includes(event.key)) { - event.preventDefault(); - this.handleTriggerClick(); - return; - } - - const menu = this.getMenu(); - - if (menu) { - const menuItems = menu.getAllItems(); - const firstMenuItem = menuItems[0]; - const lastMenuItem = menuItems[menuItems.length - 1]; - - // When up/down is pressed, we make the assumption that the user is familiar with the menu and plans to make a - // selection. Rather than toggle the panel, we focus on the menu (if one exists) and activate the first item for - // faster navigation. - if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) { - event.preventDefault(); - - // Show the menu if it's not already open - if (!this.open) { - this.show(); - - // Wait for the dropdown to open before focusing, but not the animation - await this.updateComplete; - } - - if (menuItems.length > 0) { - // Focus on the first/last menu item after showing - this.updateComplete.then(() => { - if (event.key === 'ArrowDown' || event.key === 'Home') { - menu.setCurrentItem(firstMenuItem); - firstMenuItem.focus(); - } - - if (event.key === 'ArrowUp' || event.key === 'End') { - menu.setCurrentItem(lastMenuItem); - lastMenuItem.focus(); - } - }); - } - } - } - } - - handleTriggerKeyUp(event: KeyboardEvent) { - // Prevent space from triggering a click event in Firefox - if (event.key === ' ') { - event.preventDefault(); - } - } - - handleTriggerSlotChange() { - this.updateAccessibleTrigger(); - } - - updateAccessibleTrigger() { - const assignedElements = this.trigger.assignedElements({ flatten: true }) as HTMLElement[]; - const accessibleTrigger = assignedElements[0]; - let target: HTMLElement; - - if (accessibleTrigger) { - const tagName = accessibleTrigger.tagName.toLowerCase(); - switch (tagName) { - // Web Awesome buttons have to update the internal button so it's announced correctly by screen readers - case 'wa-button': - target = (accessibleTrigger as WaButton).button; - - // Either the tag hasn't registered, or it hasn't rendered. - // So, wait for the tag to register, and then try again. - if (target === undefined || target === null) { - customElements.whenDefined(tagName).then(async () => { - await (accessibleTrigger as WaButton).updateComplete; - this.updateAccessibleTrigger(); - }); - - return; - } - break; - - default: - target = accessibleTrigger; - } - - target.setAttribute('aria-haspopup', 'true'); - target.setAttribute('aria-expanded', this.open ? 'true' : 'false'); - } - } - - /** Shows the dropdown panel. */ - async show() { - if (this.open) { - return undefined; - } - - this.open = true; - return waitForEvent(this, 'wa-after-show'); - } - - /** Hides the dropdown panel */ - async hide() { - if (!this.open) { - return undefined; - } - - this.open = false; - return waitForEvent(this, 'wa-after-hide'); - } - - /** - * Instructs the dropdown menu to reposition. Useful when the position or size of the trigger changes when the menu - * is activated. - */ - reposition() { - this.popup.reposition(); - } - - addOpenListeners() { - this.panel.addEventListener('wa-select', this.handlePanelSelect); - this.panel.addEventListener('keydown', this.handleKeyDown); - document.addEventListener('keydown', this.handleDocumentKeyDown); - document.addEventListener('mousedown', this.handleDocumentMouseDown); - } - - removeOpenListeners() { - if (this.panel) { - this.panel.removeEventListener('wa-select', this.handlePanelSelect); - this.panel.removeEventListener('keydown', this.handleKeyDown); - } - document.removeEventListener('keydown', this.handleDocumentKeyDown); - document.removeEventListener('mousedown', this.handleDocumentMouseDown); - } - - @watch('open', { waitUntilFirstUpdate: true }) - async handleOpenChange() { - if (this.disabled) { - this.open = false; - return; - } - - this.updateAccessibleTrigger(); - - if (this.open) { - // Show - const waShowEvent = new WaShowEvent(); - this.dispatchEvent(waShowEvent); - if (waShowEvent.defaultPrevented) { - this.open = false; - return; - } - - this.addOpenListeners(); - this.panel.hidden = false; - this.popup.active = true; - await animateWithClass(this.popup.popup, 'show-with-scale'); - this.dispatchEvent(new WaAfterShowEvent()); - } else { - // Hide - const waHideEvent = new WaHideEvent(); - this.dispatchEvent(waHideEvent); - if (waHideEvent.defaultPrevented) { - this.open = true; - return; - } - - this.removeOpenListeners(); - await animateWithClass(this.popup.popup, 'hide-with-scale'); - this.panel.hidden = true; - this.popup.active = false; - this.dispatchEvent(new WaAfterHideEvent()); - } - } - - render() { - return html` - - -
- -
-
- `; - } -} - -declare global { - interface HTMLElementTagNameMap { - 'wa-dropdown': WaDropdown; - } -}