mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-19 07:29:14 +00:00
Compare commits
5 Commits
pro-fixes
...
context-me
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ee519d40a | ||
|
|
6bc17d48c3 | ||
|
|
a1263f1b9d | ||
|
|
d69ebab765 | ||
|
|
0504946dac |
@@ -21,6 +21,7 @@
|
|||||||
- [Card](/components/card)
|
- [Card](/components/card)
|
||||||
- [Checkbox](/components/checkbox)
|
- [Checkbox](/components/checkbox)
|
||||||
- [Color Picker](/components/color-picker)
|
- [Color Picker](/components/color-picker)
|
||||||
|
- [Context Menu](/components/context-menu)
|
||||||
- [Details](/components/details)
|
- [Details](/components/details)
|
||||||
- [Dialog](/components/dialog)
|
- [Dialog](/components/dialog)
|
||||||
- [Divider](/components/divider)
|
- [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
|
## 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
|
### 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.
|
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>
|
</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]
|
[component-metadata:sl-dropdown]
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ _During the beta period, these restrictions may be relaxed in the event of a mis
|
|||||||
|
|
||||||
## Next
|
## 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/)
|
- 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
|
- 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
|
- 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
|
- Updated to Lit 2.0.2
|
||||||
|
|
||||||
## 2.0.0-beta.58
|
## 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) {
|
if (!this.containingElement) {
|
||||||
this.containingElement = this;
|
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() {
|
firstUpdated() {
|
||||||
@@ -131,7 +109,6 @@ export default class SlDropdown extends LitElement {
|
|||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
this.hide();
|
this.hide();
|
||||||
this.popover.destroy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
focusOnTrigger() {
|
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() {
|
handleTriggerClick() {
|
||||||
this.open ? this.hide() : this.show();
|
this.open ? this.hide() : this.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTriggerKeyDown(event: KeyboardEvent) {
|
handleTriggerKeyDown(event: KeyboardEvent) {
|
||||||
const menu = this.getMenu();
|
const menu = this.getMenu();
|
||||||
const menuItems = menu ? ([...menu.querySelectorAll('sl-menu-item')] as SlMenuItem[]) : [];
|
const menuItems = menu ? menu.getAllItems() : [];
|
||||||
const firstMenuItem = menuItems[0];
|
const firstMenuItem = menuItems[0];
|
||||||
const lastMenuItem = menuItems[menuItems.length - 1];
|
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
|
// 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
|
// selection. Rather than toggle the panel, we focus on the menu (if one exists) and activate the first item for
|
||||||
// faster navigation.
|
// faster navigation.
|
||||||
if (['ArrowDown', 'ArrowUp'].includes(event.key)) {
|
if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
// Show the menu if it's not already open
|
// Show the menu if it's not already open
|
||||||
@@ -271,14 +221,14 @@ export default class SlDropdown extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Focus on a menu item
|
// Focus on a menu item
|
||||||
if (event.key === 'ArrowDown' && firstMenuItem) {
|
if (['ArrowDown', 'Home'].includes(event.key) && firstMenuItem) {
|
||||||
const menu = this.getMenu();
|
const menu = this.getMenu();
|
||||||
menu.setCurrentItem(firstMenuItem);
|
menu.setCurrentItem(firstMenuItem);
|
||||||
firstMenuItem.focus();
|
firstMenuItem.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'ArrowUp' && lastMenuItem) {
|
if (['ArrowUp', 'End'].includes(event.key) && lastMenuItem) {
|
||||||
menu.setCurrentItem(lastMenuItem);
|
menu.setCurrentItem(lastMenuItem);
|
||||||
lastMenuItem.focus();
|
lastMenuItem.focus();
|
||||||
return;
|
return;
|
||||||
@@ -327,7 +277,7 @@ export default class SlDropdown extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Shows the dropdown panel. */
|
/** Shows the dropdown panel */
|
||||||
async show() {
|
async show() {
|
||||||
if (this.open) {
|
if (this.open) {
|
||||||
return;
|
return;
|
||||||
@@ -352,11 +302,9 @@ export default class SlDropdown extends LitElement {
|
|||||||
* is activated.
|
* is activated.
|
||||||
*/
|
*/
|
||||||
reposition() {
|
reposition() {
|
||||||
if (!this.open) {
|
if (this.popover) {
|
||||||
return;
|
this.popover.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.popover.update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@watch('open', { waitUntilFirstUpdate: true })
|
@watch('open', { waitUntilFirstUpdate: true })
|
||||||
@@ -376,7 +324,26 @@ export default class SlDropdown extends LitElement {
|
|||||||
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
||||||
|
|
||||||
await stopAnimations(this);
|
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;
|
this.panel.hidden = false;
|
||||||
const { keyframes, options } = getAnimation(this, 'dropdown.show');
|
const { keyframes, options } = getAnimation(this, 'dropdown.show');
|
||||||
await animateTo(this.panel, keyframes, options);
|
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 SlCard } from './components/card/card';
|
||||||
export { default as SlCheckbox } from './components/checkbox/checkbox';
|
export { default as SlCheckbox } from './components/checkbox/checkbox';
|
||||||
export { default as SlColorPicker } from './components/color-picker/color-picker';
|
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 SlDetails } from './components/details/details';
|
||||||
export { default as SlDialog } from './components/dialog/dialog';
|
export { default as SlDialog } from './components/dialog/dialog';
|
||||||
export { default as SlDivider } from './components/divider/divider';
|
export { default as SlDivider } from './components/divider/divider';
|
||||||
|
|||||||
Reference in New Issue
Block a user