mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 12:09:26 +00:00
rework
This commit is contained in:
100
src/components/split-panel/split-panel.styles.ts
Normal file
100
src/components/split-panel/split-panel.styles.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import { focusVisibleSelector } from '../../internal/focus-visible';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
--divider-width: 4px;
|
||||
--divider-hit-area: 12px;
|
||||
--start-min: 0%;
|
||||
--start-max: 100%;
|
||||
--end-min: 0%;
|
||||
--end-max: 100%;
|
||||
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.start,
|
||||
.end {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.start {
|
||||
background: var(--sl-color-blue-50);
|
||||
}
|
||||
|
||||
.end {
|
||||
background: var(--sl-color-orange-50);
|
||||
}
|
||||
|
||||
.divider {
|
||||
position: relative;
|
||||
flex: 0 0 var(--divider-width);
|
||||
background-color: var(--sl-color-neutral-200);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.divider:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
:host(:not([disabled])) .divider${focusVisibleSelector} {
|
||||
background-color: var(--sl-color-primary-600);
|
||||
}
|
||||
|
||||
:host([disabled]) .divider {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Horizontal */
|
||||
:host(:not([vertical])) .start {
|
||||
min-width: var(--start-min);
|
||||
max-width: var(--start-max);
|
||||
}
|
||||
|
||||
:host(:not([vertical])) .end {
|
||||
min-width: var(--end-min);
|
||||
max-width: var(--end-max);
|
||||
}
|
||||
|
||||
:host(:not([vertical], [disabled])) .divider {
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
:host(:not([vertical])) .divider::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
left: calc(var(--divider-hit-area) / -2 + var(--divider-width) / 2);
|
||||
width: var(--divider-hit-area);
|
||||
}
|
||||
|
||||
/* Vertical */
|
||||
:host([vertical]) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:host([vertical]) .start {
|
||||
min-height: var(--start-min);
|
||||
max-height: var(--start-max);
|
||||
}
|
||||
|
||||
:host([vertical]) .end {
|
||||
min-height: var(--end-min);
|
||||
max-height: var(--end-max);
|
||||
}
|
||||
|
||||
:host([vertical]:not([disabled])) .divider {
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
:host([vertical]) .divider::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: calc(var(--divider-hit-area) / -2 + var(--divider-width) / 2);
|
||||
height: var(--divider-hit-area);
|
||||
}
|
||||
`;
|
||||
13
src/components/split-panel/split-panel.test.ts
Normal file
13
src/components/split-panel/split-panel.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 SlSplitPanel from './split-panel';
|
||||
|
||||
describe('<sl-split-panel>', () => {
|
||||
it('should render a component', async () => {
|
||||
const el = await fixture(html` <sl-split-panel></sl-split-panel> `);
|
||||
|
||||
expect(el).to.exist;
|
||||
});
|
||||
});
|
||||
241
src/components/split-panel/split-panel.ts
Normal file
241
src/components/split-panel/split-panel.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { clamp } from '../../internal/math';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import styles from './split-panel.styles';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
* @status experimental
|
||||
*
|
||||
* @event sl-reposition - Emitted when the divider is repositioned.
|
||||
* @event {{ entries: ResizeObserverEntry[] }} sl-resize - Emitted when the container is resized.
|
||||
*
|
||||
* @slot start - The start panel.
|
||||
* @slot end - The end panel.
|
||||
*
|
||||
* @cssproperty [--divider-width=4px] - The width of the visible divider.
|
||||
* @cssproperty [--divider-hit-area=12px] - The invisible area around the divider where dragging can occur.
|
||||
*/
|
||||
@customElement('sl-split-panel')
|
||||
export default class SlSplitPanel extends LitElement {
|
||||
static styles = styles;
|
||||
|
||||
private resizeObserver: ResizeObserver;
|
||||
private size: number;
|
||||
|
||||
@query('.divider') divider: HTMLElement;
|
||||
|
||||
/**
|
||||
* The current position of the divider from the fixed panel's edge. Defaults to 50% of the container's intial size.
|
||||
*/
|
||||
@property({ type: Number, reflect: true }) position: number;
|
||||
|
||||
/** Draws the split panel in a vertical orientation with the start and end panels stacked. */
|
||||
@property({ type: Boolean, reflect: true }) vertical = false;
|
||||
|
||||
/** Disables resizing on the split panel. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
/**
|
||||
* When the host element is resized, the fixed panel will maintain its size and the other panel will grow or shrink to
|
||||
* fit the remaining space.
|
||||
*/
|
||||
@property() fixed: 'start' | 'end' = 'start';
|
||||
|
||||
/**
|
||||
* One or more space-separated values at which the divider should snap. Values can be in pixels or percentages, e.g.
|
||||
* `"100px 50%"`.
|
||||
*/
|
||||
@property() snap: string;
|
||||
|
||||
/** How close the divider must be to a snap point until snapping occurs. */
|
||||
@property({ type: Number, attribute: 'snap-threshold' }) snapThreshold = 12;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.resizeObserver = new ResizeObserver(entries => this.handleResize(entries));
|
||||
this.updateComplete.then(() => this.resizeObserver.observe(this));
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.resizeObserver.unobserve(this);
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
const { width, height } = this.getBoundingClientRect();
|
||||
this.size = this.vertical ? height : width;
|
||||
|
||||
if (!this.position) {
|
||||
this.position = this.size / 2;
|
||||
}
|
||||
}
|
||||
|
||||
handleDrag(event: MouseEvent | TouchEvent) {
|
||||
const isMouseEvent = event instanceof MouseEvent;
|
||||
const originalX = isMouseEvent ? event.pageX : event.changedTouches[0].pageX;
|
||||
const originalY = isMouseEvent ? event.pageY : event.changedTouches[0].pageY;
|
||||
const original = this.vertical ? originalY : originalX;
|
||||
const originalPosition = Number(this.position);
|
||||
|
||||
const move = (event: MouseEvent | TouchEvent) => {
|
||||
const isMouseEvent = event instanceof MouseEvent;
|
||||
const currentX = isMouseEvent ? event.pageX : event.changedTouches[0].pageX;
|
||||
const currentY = isMouseEvent ? event.pageY : event.changedTouches[0].pageY;
|
||||
const current = this.vertical ? currentY : currentX;
|
||||
let delta = this.fixed === 'end' ? original - current : current - original;
|
||||
let newPosition = originalPosition + delta;
|
||||
|
||||
// Check snap points
|
||||
if (this.snap) {
|
||||
const snaps = this.snap.split(' ');
|
||||
|
||||
snaps.map(value => {
|
||||
let snapPoint: number;
|
||||
|
||||
if (value.endsWith('%')) {
|
||||
snapPoint = this.size * (parseFloat(value) / 100);
|
||||
} else {
|
||||
snapPoint = parseFloat(value);
|
||||
}
|
||||
|
||||
if (newPosition >= snapPoint - this.snapThreshold && newPosition <= snapPoint + this.snapThreshold) {
|
||||
newPosition = snapPoint;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.position = clamp(newPosition, 0, this.size);
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
document.removeEventListener('mousemove', move);
|
||||
document.removeEventListener('touchmove', move);
|
||||
document.removeEventListener('mouseup', stop);
|
||||
document.removeEventListener('touchend', stop);
|
||||
};
|
||||
|
||||
if (!this.disabled) {
|
||||
document.addEventListener('mousemove', move);
|
||||
document.addEventListener('touchmove', move);
|
||||
document.addEventListener('mouseup', stop);
|
||||
document.addEventListener('touchend', stop);
|
||||
}
|
||||
|
||||
// Prevent text selection
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
handleKeyDown(event: KeyboardEvent) {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) {
|
||||
const incr = event.shiftKey ? 10 : 1;
|
||||
let newPercentage = this.getPositionPercentage();
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if ((event.key === 'ArrowLeft' && !this.vertical) || (event.key === 'ArrowUp' && this.vertical)) {
|
||||
newPercentage -= incr;
|
||||
}
|
||||
|
||||
if ((event.key === 'ArrowRight' && !this.vertical) || (event.key === 'ArrowDown' && this.vertical)) {
|
||||
newPercentage += incr;
|
||||
}
|
||||
|
||||
if (event.key === 'Home') {
|
||||
newPercentage = 0;
|
||||
}
|
||||
|
||||
if (event.key === 'End') {
|
||||
newPercentage = 100;
|
||||
}
|
||||
|
||||
newPercentage = clamp(newPercentage, 0, 100);
|
||||
|
||||
this.setPositionPercentage(newPercentage);
|
||||
}
|
||||
}
|
||||
|
||||
@watch('position')
|
||||
handlePositionChange() {
|
||||
emit(this, 'sl-reposition');
|
||||
}
|
||||
|
||||
handleResize(entries: ResizeObserverEntry[]) {
|
||||
const { width, height } = entries[0].contentRect;
|
||||
this.size = this.vertical ? height : width;
|
||||
|
||||
emit(this, 'sl-resize', { detail: { entries } });
|
||||
}
|
||||
|
||||
/** Gets the divider's position as a percentage of the container's size. */
|
||||
getPositionPercentage() {
|
||||
if (this.size === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (this.position / this.size) * 100;
|
||||
}
|
||||
|
||||
/** Sets the divider position as a percentage of the container's size. */
|
||||
setPositionPercentage(value: number) {
|
||||
this.position = clamp(this.size * (value / 100), 0, this.size);
|
||||
}
|
||||
|
||||
render() {
|
||||
let start: string;
|
||||
let end: string;
|
||||
|
||||
// TODO - min / max
|
||||
// TODO - custom divider styles + handle
|
||||
|
||||
if (this.fixed === 'end') {
|
||||
start = `1 1 0%`;
|
||||
end = `0 0 calc((${this.position}px - var(--divider-width) / 2)`;
|
||||
} else {
|
||||
start = `0 0 calc(${this.position}px - var(--divider-width) / 2)`;
|
||||
end = `1 1 0%`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="start"
|
||||
style=${styleMap({
|
||||
flex: start
|
||||
})}
|
||||
>
|
||||
<slot name="start"></slot>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="divider"
|
||||
tabindex=${ifDefined(this.disabled ? undefined : '0')}
|
||||
@keydown=${this.handleKeyDown}
|
||||
@mousedown=${this.handleDrag}
|
||||
@touchstart=${this.handleDrag}
|
||||
></div>
|
||||
|
||||
<div
|
||||
class="end"
|
||||
style=${styleMap({
|
||||
flex: end
|
||||
})}
|
||||
>
|
||||
<slot name="end"></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-split-panel': SlSplitPanel;
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ export { default as SlResponsiveMedia } from './components/responsive-media/resp
|
||||
export { default as SlSelect } from './components/select/select';
|
||||
export { default as SlSkeleton } from './components/skeleton/skeleton';
|
||||
export { default as SlSpinner } from './components/spinner/spinner';
|
||||
export { default as SlSplitPanel } from './components/split-panel/split-panel';
|
||||
export { default as SlSwitch } from './components/switch/switch';
|
||||
export { default as SlTab } from './components/tab/tab';
|
||||
export { default as SlTabGroup } from './components/tab-group/tab-group';
|
||||
|
||||
Reference in New Issue
Block a user