pull/629/head
Cory LaViska 2021-12-22 18:32:27 -05:00
rodzic 33accf65ef
commit a5b7f8fd6b
6 zmienionych plików z 541 dodań i 0 usunięć

Wyświetl plik

@ -58,6 +58,7 @@
- [Tag](/components/tag)
- [Textarea](/components/textarea)
- [Tooltip](/components/tooltip)
- [Split Panel](/components/split-panel)
<!--plop:component-->
- Utilities

Wyświetl plik

@ -0,0 +1,185 @@
# Split Panel
[component-header:sl-split-panel]
Split panels display two panels alongside each other, often allowing the user to resize them.
```html preview
<sl-split-panel>
<div slot="start">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quaerat, suscipit animi. Exercitationem, modi tenetur, voluptatibus magnam qui excepturi quasi autem et odit, recusandae obcaecati! Quaerat possimus facilis tempora consequatur officia?
</div>
<div slot="end">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quaerat, suscipit animi. Exercitationem, modi tenetur, voluptatibus magnam qui excepturi quasi autem et odit, recusandae obcaecati! Quaerat possimus facilis tempora consequatur officia?
</div>
</sl-split-panel>
```
## Examples
### Initial Position
To set the initial position of the split in pixels, use the `position` attribute. The value must be in pixels, but if you need to set it as a percentage, use the `setPositionPercentage()` method instead.
```html preview
<sl-split-panel position="150">
<div slot="start">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quaerat, suscipit animi. Exercitationem, modi tenetur, voluptatibus magnam qui excepturi quasi autem et odit, recusandae obcaecati! Quaerat possimus facilis tempora consequatur officia?
</div>
<div slot="end">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quaerat, suscipit animi. Exercitationem, modi tenetur, voluptatibus magnam qui excepturi quasi autem et odit, recusandae obcaecati! Quaerat possimus facilis tempora consequatur officia?
</div>
</sl-split-panel>
```
### Vertical
Add the `vertical` attribute to render the split panel in a vertical orientation where the start and end panels are stacked.
```html preview
<sl-split-panel vertical style="height: 400px;">
<div slot="start">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quaerat, suscipit animi. Exercitationem, modi tenetur, voluptatibus magnam qui excepturi quasi autem et odit, recusandae obcaecati! Quaerat possimus facilis tempora consequatur officia?
</div>
<div slot="end">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quaerat, suscipit animi. Exercitationem, modi tenetur, voluptatibus magnam qui excepturi quasi autem et odit, recusandae obcaecati! Quaerat possimus facilis tempora consequatur officia?
</div>
</sl-split-panel>
```
### Snapping
To snap panels at specific locations, add the `snap` attribute with one or more space-separated values. Values can be in pixels or percentages. For example, to snap the panel at `100px` and `50%`, use `snap="100px 50%"`. To customize how close the divider must be before snapping, use the `snap-threshold` attribute.
```html preview
<div class="split-panel-snapping">
<sl-split-panel snap="100px 50%">
<div slot="start">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quaerat, suscipit animi. Exercitationem, modi tenetur, voluptatibus magnam qui excepturi quasi autem et odit, recusandae obcaecati! Quaerat possimus facilis tempora consequatur officia?
</div>
<div slot="end">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quaerat, suscipit animi. Exercitationem, modi tenetur, voluptatibus magnam qui excepturi quasi autem et odit, recusandae obcaecati! Quaerat possimus facilis tempora consequatur officia?
</div>
</sl-split-panel>
<div class="split-panel-snapping-dots"></div>
</div>
<style>
.split-panel-snapping {
position: relative;
}
.split-panel-snapping-dots::before,
.split-panel-snapping-dots::after {
content: '';
position: absolute;
bottom: -12px;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--sl-color-neutral-400);
transform: translateX(-3px);
}
.split-panel-snapping-dots::before {
left: 100px;
}
.split-panel-snapping-dots::after {
left: 50%;
}
</style>
```
### Disabled
Add the `disabled` attribute to prevent the split panel from being resized.
```html preview
<sl-split-panel disabled>
<div slot="start">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quaerat, suscipit animi. Exercitationem, modi tenetur, voluptatibus magnam qui excepturi quasi autem et odit, recusandae obcaecati! Quaerat possimus facilis tempora consequatur officia?
</div>
<div slot="end">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quaerat, suscipit animi. Exercitationem, modi tenetur, voluptatibus magnam qui excepturi quasi autem et odit, recusandae obcaecati! Quaerat possimus facilis tempora consequatur officia?
</div>
</sl-split-panel>
```
### Setting the Fixed Panel
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. Try resizing the example below with each option and notice how panels respond.
```html preview
<div class="split-panel-fixed">
<sl-split-panel fixed="start">
<div slot="start">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quaerat, suscipit animi. Exercitationem, modi tenetur, voluptatibus magnam qui excepturi quasi autem et odit, recusandae obcaecati! Quaerat possimus facilis tempora consequatur officia?
</div>
<div slot="end">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quaerat, suscipit animi. Exercitationem, modi tenetur, voluptatibus magnam qui excepturi quasi autem et odit, recusandae obcaecati! Quaerat possimus facilis tempora consequatur officia?
</div>
</sl-split-panel>
<sl-select value="start" style="max-width: 200px; margin-top: 1rem;">
<sl-menu-item value="start">Start</sl-menu-item>
<sl-menu-item value="end">End</sl-menu-item>
</sl-select>
</div>
<script>
const container = document.querySelector('.split-panel-fixed');
const splitPanel = container.querySelector('sl-split-panel');
const select = container.querySelector('sl-select');
select.addEventListener('sl-change', () => splitPanel.fixed = select.value);
</script>
```
### Minimum and Maximum Sizes
To set a minimum or maximum size of each panel, use the `--start-min`, `--start-max`, `--end-min`, and `--end-max` custom properties.
```html preview
<sl-split-panel style="--end-min: 25%; --end-max: 75%;">
<div slot="start">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quaerat, suscipit animi. Exercitationem, modi tenetur, voluptatibus magnam qui excepturi quasi autem et odit, recusandae obcaecati! Quaerat possimus facilis tempora consequatur officia?
</div>
<div slot="end">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quaerat, suscipit animi. Exercitationem, modi tenetur, voluptatibus magnam qui excepturi quasi autem et odit, recusandae obcaecati! Quaerat possimus facilis tempora consequatur officia?
</div>
</sl-split-panel>
```
### Nested Split Panels
Create complex layouts that can be resized independently by nesting split panels.
```html preview
<sl-split-panel>
<div slot="start">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quaerat, suscipit animi. Exercitationem, modi tenetur, voluptatibus magnam qui excepturi quasi autem et odit, recusandae obcaecati! Quaerat possimus facilis tempora consequatur officia?
</div>
<div slot="end">
<sl-split-panel vertical style="height: 400px;">
<div slot="start">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quaerat, suscipit animi. Exercitationem, modi tenetur, voluptatibus magnam qui excepturi quasi autem et odit, recusandae obcaecati! Quaerat possimus facilis tempora consequatur officia?
</div>
<div slot="end">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quaerat, suscipit animi. Exercitationem, modi tenetur, voluptatibus magnam qui excepturi quasi autem et odit, recusandae obcaecati! Quaerat possimus facilis tempora consequatur officia?
</div>
</sl-split-panel>
</div>
</sl-split-panel>
```
### Customizing the Divider
You can target the `divider` part to apply CSS properties to the divider. Optionally, you can slot an element into the `handle` slot to show a handle.
```html preview
TODO
```
[component-metadata:sl-split-panel]

Wyświetl plik

@ -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);
}
`;

Wyświetl plik

@ -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;
});
});

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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';