mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 20:19:13 +00:00
Compare commits
5 Commits
select-tag
...
context-me
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ee519d40a | ||
|
|
6bc17d48c3 | ||
|
|
a1263f1b9d | ||
|
|
d69ebab765 | ||
|
|
0504946dac |
@@ -21,6 +21,7 @@
|
||||
- [Card](/components/card)
|
||||
- [Checkbox](/components/checkbox)
|
||||
- [Color Picker](/components/color-picker)
|
||||
- [Context Menu](/components/context-menu)
|
||||
- [Details](/components/details)
|
||||
- [Dialog](/components/dialog)
|
||||
- [Divider](/components/divider)
|
||||
|
||||
140
docs/components/context-menu.md
Normal file
140
docs/components/context-menu.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# Context Menu
|
||||
|
||||
[component-header:sl-context-menu]
|
||||
|
||||
Context menus offer additional options through a menu that opens at the pointer's location, usually activated by a right-click.
|
||||
|
||||
Context menus are designed to work with [menus](/components/menu) and [menu items](/components/menu-item). The menu must include `slot="menu"`. Other content you provide will be part of the context menu's target area.
|
||||
|
||||
```html preview
|
||||
<sl-context-menu>
|
||||
<div style="height: 200px; background: rgb(var(--sl-color-neutral-100)); display: flex; align-items: center; justify-content: center; padding: 1rem;">
|
||||
Right-click to activate the context menu
|
||||
</div>
|
||||
|
||||
<sl-menu slot="menu">
|
||||
<sl-menu-item value="undo">Undo</sl-menu-item>
|
||||
<sl-menu-item value="redo">Redo</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
<sl-menu-item value="delete">Delete</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-context-menu>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Handling Selections
|
||||
|
||||
The [menu component](/components/menu) emits an `sl-select` event when a menu item is selected. You can use this to handle selections. The selected item will be available in `event.detail.item`.
|
||||
|
||||
```html preview
|
||||
<div class="context-menu-selections">
|
||||
<sl-context-menu>
|
||||
<div style="height: 200px; background: rgb(var(--sl-color-neutral-100)); display: flex; align-items: center; justify-content: center; padding: 1rem;">
|
||||
Right-click to activate the context menu
|
||||
</div>
|
||||
|
||||
<sl-menu slot="menu">
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-context-menu>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.context-menu-selections');
|
||||
const menu = container.querySelector('sl-menu');
|
||||
const result = container.querySelector('.result');
|
||||
|
||||
menu.addEventListener('sl-select', event => {
|
||||
console.log(`You selected: ${event.detail.item.value}`);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### Inline
|
||||
|
||||
The context menu uses `display: contents`, so it will assume the shape of the content you slot in.
|
||||
|
||||
```html preview
|
||||
<sl-context-menu>
|
||||
<span style="background: rgb(var(--sl-color-neutral-100)); padding: .5rem 1rem;">
|
||||
Right-click here
|
||||
</span>
|
||||
|
||||
<sl-menu slot="menu">
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-context-menu>
|
||||
```
|
||||
|
||||
### Placement
|
||||
|
||||
The preferred placement of the context menu can be set with the `placement` attribute. Note that the actual position may vary to ensure the menu remains in the viewport.
|
||||
|
||||
```html preview
|
||||
<sl-context-menu placement="top-end">
|
||||
<div style="height: 200px; background: rgb(var(--sl-color-neutral-100)); display: flex; align-items: center; justify-content: center; padding: 1rem;">
|
||||
Right-click to activate the context menu
|
||||
</div>
|
||||
|
||||
<sl-menu slot="menu">
|
||||
<sl-menu-item value="undo">Undo</sl-menu-item>
|
||||
<sl-menu-item value="redo">Redo</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
<sl-menu-item value="delete">Delete</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-context-menu>
|
||||
```
|
||||
|
||||
### Detecting the Target Item
|
||||
|
||||
A single context menu can wrap a number of items. To detect the item that activated the context menu...
|
||||
|
||||
TODO
|
||||
|
||||
```html preview
|
||||
<div class="context-menu-detecting">
|
||||
<sl-context-menu>
|
||||
<ul>
|
||||
<li>Item 1</li>
|
||||
<li>Item 2</li>
|
||||
<li>Item 3</li>
|
||||
<li>Item 4</li>
|
||||
<li>Item 5</li>
|
||||
</ul>
|
||||
|
||||
<sl-menu slot="menu">
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-context-menu>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.context-menu-detecting ul {
|
||||
max-width: 300px;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.context-menu-detecting li {
|
||||
background: rgb(var(--sl-color-neutral-100));
|
||||
padding: .5rem 1rem;
|
||||
margin: 0 0 2px 0;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
[component-metadata:sl-context-menu]
|
||||
@@ -33,6 +33,33 @@ Dropdowns are designed to work well with [menus](/components/menu) to provide a
|
||||
|
||||
## Examples
|
||||
|
||||
### Getting the Selected Item
|
||||
|
||||
When dropdowns are used with [menus](/components/menu), you can listen for the `sl-select` 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 preview
|
||||
<div class="dropdown-selection">
|
||||
<sl-dropdown>
|
||||
<sl-button slot="trigger" caret>Edit</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.dropdown-selection');
|
||||
const dropdown = container.querySelector('sl-dropdown');
|
||||
|
||||
dropdown.addEventListener('sl-select', event => {
|
||||
const selectedItem = event.detail.item;
|
||||
console.log(selectedItem.value);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### 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.
|
||||
@@ -121,57 +148,4 @@ Dropdown panels will be clipped if they're inside a container that has `overflow
|
||||
</style>
|
||||
```
|
||||
|
||||
### Getting the Selected Item
|
||||
|
||||
When dropdowns are used with [menus](/components/menu), you can listen for the `sl-select` 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 preview
|
||||
<div class="dropdown-selection">
|
||||
<sl-dropdown>
|
||||
<sl-button slot="trigger" caret>Edit</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.dropdown-selection');
|
||||
const dropdown = container.querySelector('sl-dropdown');
|
||||
|
||||
dropdown.addEventListener('sl-select', event => {
|
||||
const selectedItem = event.detail.item;
|
||||
console.log(selectedItem.value);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
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 preview
|
||||
<div class="dropdown-selection-alt">
|
||||
<sl-dropdown>
|
||||
<sl-button slot="trigger" caret>Edit</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.dropdown-selection-alt');
|
||||
const cut = container.querySelector('sl-menu-item[value="cut"]');
|
||||
const copy = container.querySelector('sl-menu-item[value="copy"]');
|
||||
const paste = container.querySelector('sl-menu-item[value="paste"]');
|
||||
|
||||
cut.addEventListener('click', () => console.log('cut'));
|
||||
copy.addEventListener('click', () => console.log('copy'));
|
||||
paste.addEventListener('click', () => console.log('paste'));
|
||||
</script>
|
||||
```
|
||||
|
||||
[component-metadata:sl-dropdown]
|
||||
|
||||
@@ -8,9 +8,11 @@ _During the beta period, these restrictions may be relaxed in the event of a mis
|
||||
|
||||
## Next
|
||||
|
||||
- Added experimental `<sl-context-menu>` component
|
||||
- Added eye dropper to `<sl-color-picker>` when the browser supports the [EyeDropper API](https://wicg.github.io/eyedropper-api/)
|
||||
- Fixed a bug in `<sl-button-group>` where buttons groups with only one button would have an incorrect border radius
|
||||
- Improved the `<sl-color-picker>` trigger's border in dark mode
|
||||
- Refactored positioning logic in `<sl-dropdown>` so Popper is only active when the menu is open
|
||||
- Updated to Lit 2.0.2
|
||||
|
||||
## 2.0.0-beta.58
|
||||
|
||||
43
src/components/context-menu/context-menu.styles.ts
Normal file
43
src/components/context-menu/context-menu.styles.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
::slotted(sl-menu) {
|
||||
min-width: 180px;
|
||||
background: rgb(var(--sl-panel-background-color));
|
||||
border: solid var(--sl-panel-border-width) rgb(var(--sl-panel-border-color));
|
||||
border-radius: var(--sl-border-radius-medium);
|
||||
box-shadow: var(--sl-shadow-large);
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
position: relative;
|
||||
z-index: var(--sl-z-index-dropdown);
|
||||
}
|
||||
|
||||
.context-menu__locater {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dropdown__positioner {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.context-menu__menu {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: all;
|
||||
}
|
||||
`;
|
||||
13
src/components/context-menu/context-menu.test.ts
Normal file
13
src/components/context-menu/context-menu.test.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
// import sinon from 'sinon';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlContextMenu from './context-menu';
|
||||
|
||||
describe('<sl-context-menu>', () => {
|
||||
it('should render a component', async () => {
|
||||
const el = await fixture(html` <sl-context-menu></sl-context-menu> `);
|
||||
|
||||
expect(el).to.exist;
|
||||
});
|
||||
});
|
||||
292
src/components/context-menu/context-menu.ts
Normal file
292
src/components/context-menu/context-menu.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { emit, waitForEvent } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { Instance as PopperInstance, createPopper } from '@popperjs/core/dist/esm';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry';
|
||||
import type SlMenu from '../menu/menu';
|
||||
import styles from './context-menu.styles';
|
||||
|
||||
import '../menu/menu';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
* @status experimental
|
||||
*
|
||||
* @dependency sl-menu
|
||||
*
|
||||
* @event sl-event-name - Emitted as an example.
|
||||
*
|
||||
* @slot - Content that will activate the context menu when right-clicked.
|
||||
* @slot menu - The menu to show when the context menu is activated, an `<sl-menu>` element.
|
||||
*
|
||||
* @event sl-show - Emitted when the context menu opens.
|
||||
* @event sl-after-show - Emitted after the context menu opens and all animations are complete.
|
||||
* @event sl-hide - Emitted when the context menu closes.
|
||||
* @event sl-after-hide - Emitted after the context menu closes and all animations are complete.
|
||||
*
|
||||
* @animation contextMenu.show - The animation to use when showing the context menu.
|
||||
* @animation contextMenu.hide - The animation to use when hiding the context menu.
|
||||
*/
|
||||
@customElement('sl-context-menu')
|
||||
export default class SlContextMenu extends LitElement {
|
||||
static styles = styles;
|
||||
|
||||
@query('.context-menu') wrapper: HTMLElement;
|
||||
@query('.context-menu__locater') locater: HTMLElement;
|
||||
@query('.context-menu__menu') menu: HTMLSlotElement;
|
||||
@query('.context-menu__positioner') positioner: HTMLElement;
|
||||
|
||||
private popover: PopperInstance;
|
||||
|
||||
/**
|
||||
* The preferred placement of the context menu. Note that the actual placement may vary as needed to keep the menu
|
||||
* inside of the viewport.
|
||||
*/
|
||||
@property() placement:
|
||||
| 'top'
|
||||
| 'top-start'
|
||||
| 'top-end'
|
||||
| 'right'
|
||||
| 'right-start'
|
||||
| 'right-end'
|
||||
| 'bottom'
|
||||
| 'bottom-start'
|
||||
| 'bottom-end'
|
||||
| 'left'
|
||||
| 'left-start'
|
||||
| 'left-end' = 'bottom-start';
|
||||
|
||||
/** Disables the context menu so it won't show when triggered. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
/** The distance in pixels from which to offset the context menu away from its target. */
|
||||
@property({ type: Number }) distance = 0;
|
||||
|
||||
/** Indicates whether or not the context menu is open. You can use this in lieu of the show/hide methods. */
|
||||
@property({ type: Boolean, reflect: true }) open = false;
|
||||
|
||||
/** The distance in pixels from which to offset the context menu along its target. */
|
||||
@property({ type: Number }) skidding = 0;
|
||||
|
||||
/**
|
||||
* Enable this option to prevent the menu from being clipped when the component is placed inside a container with
|
||||
* `overflow: auto|hidden|scroll`.
|
||||
*/
|
||||
@property({ type: Boolean }) hoist = false;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.handleDocumentKeyDown = this.handleDocumentKeyDown.bind(this);
|
||||
this.handleDocumentMouseDown = this.handleDocumentMouseDown.bind(this);
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.menu.hidden = !this.open;
|
||||
}
|
||||
|
||||
getMenu() {
|
||||
const slot = this.menu.querySelector('slot')!;
|
||||
return slot.assignedElements({ flatten: true }).filter(el => el.tagName.toLowerCase() === 'sl-menu')[0] as SlMenu;
|
||||
}
|
||||
|
||||
async handleContextMenu(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
const targetRect = target.getBoundingClientRect();
|
||||
const wrapperRect = this.wrapper.getBoundingClientRect();
|
||||
const { offsetX, offsetY } = event;
|
||||
const x = targetRect.left + offsetX - wrapperRect.left;
|
||||
const y = targetRect.top + offsetY - wrapperRect.top;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if (this.open) {
|
||||
await this.hide();
|
||||
}
|
||||
|
||||
this.show(x, y);
|
||||
}
|
||||
|
||||
handleDocumentKeyDown(event: KeyboardEvent) {
|
||||
const menu = this.getMenu();
|
||||
const menuItems = menu ? menu.getAllItems() : [];
|
||||
const firstMenuItem = menuItems[0];
|
||||
const lastMenuItem = menuItems[menuItems.length - 1];
|
||||
|
||||
// Close when escape is pressed
|
||||
if (event.key === 'Escape') {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// Forward key presses that don't originate from the menu to allow keyboard selection and type-to-select
|
||||
if (menu && !event.composedPath().includes(this.menu)) {
|
||||
// Focus on a menu item
|
||||
if (['ArrowDown', 'Home'].includes(event.key) && firstMenuItem) {
|
||||
event.preventDefault();
|
||||
const menu = this.getMenu();
|
||||
menu.setCurrentItem(firstMenuItem);
|
||||
firstMenuItem.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (['ArrowUp', 'End'].includes(event.key) && lastMenuItem) {
|
||||
event.preventDefault();
|
||||
menu.setCurrentItem(lastMenuItem);
|
||||
lastMenuItem.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Other keys bring focus to the menu and initiate type-to-select behavior
|
||||
const ignoredKeys = ['Tab', 'Shift', 'Meta', 'Ctrl', 'Alt'];
|
||||
if (!ignoredKeys.includes(event.key)) {
|
||||
menu.typeToSelect(event.key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleDocumentMouseDown(event: MouseEvent) {
|
||||
const path = event.composedPath() as Array<EventTarget>;
|
||||
|
||||
//
|
||||
// Close the context menu when clicking outside of it. We use a setTimeout here because mousedown fires before
|
||||
// contextmenu and, if the menu is already open and the user-right clicks again, we want the menu to re-open in the
|
||||
// new position instead of closing.
|
||||
//
|
||||
setTimeout(() => {
|
||||
if (this.open && !path.includes(this.menu)) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleMenuSelect() {
|
||||
// Close the context menu when a menu item is selected
|
||||
this.hide();
|
||||
}
|
||||
|
||||
@watch('open', { waitUntilFirstUpdate: true })
|
||||
async handleOpenChange() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.open) {
|
||||
// Show
|
||||
emit(this, 'sl-show');
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
|
||||
await stopAnimations(this);
|
||||
|
||||
this.popover = createPopper(this.locater, this.positioner, {
|
||||
placement: this.placement,
|
||||
strategy: this.hoist ? 'fixed' : 'absolute',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
boundary: 'viewport'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [this.skidding, this.distance]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
this.menu.hidden = false;
|
||||
const { keyframes, options } = getAnimation(this, 'contextMenu.show');
|
||||
await animateTo(this.menu, keyframes, options);
|
||||
|
||||
emit(this, 'sl-after-show');
|
||||
} else {
|
||||
// Hide
|
||||
emit(this, 'sl-hide');
|
||||
document.removeEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
|
||||
await stopAnimations(this);
|
||||
const { keyframes, options } = getAnimation(this, 'contextMenu.hide');
|
||||
await animateTo(this.menu, keyframes, options);
|
||||
|
||||
this.menu.hidden = true;
|
||||
this.locater.style.top = '0px';
|
||||
this.locater.style.left = '0px';
|
||||
this.popover.destroy();
|
||||
|
||||
emit(this, 'sl-after-hide');
|
||||
}
|
||||
}
|
||||
|
||||
/** Shows the context menu */
|
||||
async show(offsetX?: number, offsetY?: number) {
|
||||
if (this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.locater.style.top = `${offsetY || 0}px`;
|
||||
this.locater.style.left = `${offsetX || 0}px`;
|
||||
this.open = true;
|
||||
|
||||
return waitForEvent(this, 'sl-after-show');
|
||||
}
|
||||
|
||||
/** Hides the dropdown panel */
|
||||
async hide() {
|
||||
if (!this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.open = false;
|
||||
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="context-menu">
|
||||
<slot @contextmenu=${this.handleContextMenu}></slot>
|
||||
|
||||
<div class="context-menu__locater"></div>
|
||||
|
||||
<!-- Position the menu with a wrapper since the popover makes use of translate. This let's us add animations
|
||||
on the menu without interfering with the position. -->
|
||||
<div class="context-menu__positioner">
|
||||
<div class="context-menu__menu" hidden @sl-select=${this.handleMenuSelect}>
|
||||
<slot name="menu"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
setDefaultAnimation('contextMenu.show', {
|
||||
keyframes: [
|
||||
{ opacity: 0, transform: 'scale(0.9)' },
|
||||
{ opacity: 1, transform: 'scale(1)' }
|
||||
],
|
||||
options: { duration: 50, easing: 'ease' }
|
||||
});
|
||||
|
||||
setDefaultAnimation('contextMenu.hide', {
|
||||
keyframes: [
|
||||
{ opacity: 1, transform: 'scale(1)' },
|
||||
{ opacity: 0, transform: 'scale(0.9)' }
|
||||
],
|
||||
options: { duration: 150, easing: 'ease' }
|
||||
});
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-context-menu': SlContextMenu;
|
||||
}
|
||||
}
|
||||
@@ -100,28 +100,6 @@ export default class SlDropdown extends LitElement {
|
||||
if (!this.containingElement) {
|
||||
this.containingElement = this;
|
||||
}
|
||||
|
||||
// Create the popover after render
|
||||
this.updateComplete.then(() => {
|
||||
this.popover = createPopper(this.trigger, this.positioner, {
|
||||
placement: this.placement,
|
||||
strategy: this.hoist ? 'fixed' : 'absolute',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
boundary: 'viewport'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [this.skidding, this.distance]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
@@ -131,7 +109,6 @@ export default class SlDropdown extends LitElement {
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.hide();
|
||||
this.popover.destroy();
|
||||
}
|
||||
|
||||
focusOnTrigger() {
|
||||
@@ -207,40 +184,13 @@ export default class SlDropdown extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
@watch('distance')
|
||||
@watch('hoist')
|
||||
@watch('placement')
|
||||
@watch('skidding')
|
||||
handlePopoverOptionsChange() {
|
||||
if (this.popover) {
|
||||
this.popover.setOptions({
|
||||
placement: this.placement,
|
||||
strategy: this.hoist ? 'fixed' : 'absolute',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
boundary: 'viewport'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [this.skidding, this.distance]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleTriggerClick() {
|
||||
this.open ? this.hide() : this.show();
|
||||
}
|
||||
|
||||
handleTriggerKeyDown(event: KeyboardEvent) {
|
||||
const menu = this.getMenu();
|
||||
const menuItems = menu ? ([...menu.querySelectorAll('sl-menu-item')] as SlMenuItem[]) : [];
|
||||
const menuItems = menu ? menu.getAllItems() : [];
|
||||
const firstMenuItem = menuItems[0];
|
||||
const lastMenuItem = menuItems[menuItems.length - 1];
|
||||
|
||||
@@ -262,7 +212,7 @@ export default class SlDropdown extends LitElement {
|
||||
// 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'].includes(event.key)) {
|
||||
if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
|
||||
event.preventDefault();
|
||||
|
||||
// Show the menu if it's not already open
|
||||
@@ -271,14 +221,14 @@ export default class SlDropdown extends LitElement {
|
||||
}
|
||||
|
||||
// Focus on a menu item
|
||||
if (event.key === 'ArrowDown' && firstMenuItem) {
|
||||
if (['ArrowDown', 'Home'].includes(event.key) && firstMenuItem) {
|
||||
const menu = this.getMenu();
|
||||
menu.setCurrentItem(firstMenuItem);
|
||||
firstMenuItem.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowUp' && lastMenuItem) {
|
||||
if (['ArrowUp', 'End'].includes(event.key) && lastMenuItem) {
|
||||
menu.setCurrentItem(lastMenuItem);
|
||||
lastMenuItem.focus();
|
||||
return;
|
||||
@@ -327,7 +277,7 @@ export default class SlDropdown extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/** Shows the dropdown panel. */
|
||||
/** Shows the dropdown panel */
|
||||
async show() {
|
||||
if (this.open) {
|
||||
return;
|
||||
@@ -352,11 +302,9 @@ export default class SlDropdown extends LitElement {
|
||||
* is activated.
|
||||
*/
|
||||
reposition() {
|
||||
if (!this.open) {
|
||||
return;
|
||||
if (this.popover) {
|
||||
this.popover.update();
|
||||
}
|
||||
|
||||
this.popover.update();
|
||||
}
|
||||
|
||||
@watch('open', { waitUntilFirstUpdate: true })
|
||||
@@ -376,7 +324,26 @@ export default class SlDropdown extends LitElement {
|
||||
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
|
||||
await stopAnimations(this);
|
||||
this.popover.update();
|
||||
|
||||
this.popover = createPopper(this.trigger, this.positioner, {
|
||||
placement: this.placement,
|
||||
strategy: this.hoist ? 'fixed' : 'absolute',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
boundary: 'viewport'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [this.skidding, this.distance]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
this.panel.hidden = false;
|
||||
const { keyframes, options } = getAnimation(this, 'dropdown.show');
|
||||
await animateTo(this.panel, keyframes, options);
|
||||
|
||||
@@ -11,6 +11,7 @@ export { default as SlButtonGroup } from './components/button-group/button-group
|
||||
export { default as SlCard } from './components/card/card';
|
||||
export { default as SlCheckbox } from './components/checkbox/checkbox';
|
||||
export { default as SlColorPicker } from './components/color-picker/color-picker';
|
||||
export { default as SlContextMenu } from './components/context-menu/context-menu';
|
||||
export { default as SlDetails } from './components/details/details';
|
||||
export { default as SlDialog } from './components/dialog/dialog';
|
||||
export { default as SlDivider } from './components/divider/divider';
|
||||
|
||||
Reference in New Issue
Block a user