kopia lustrzana https://github.com/shoelace-style/shoelace
SlSplitPanel `snap` improvements. (#2340)
* (sl-split-panel) Add repeat expression and function expression to [snap]. * (sl-split-panel) Improve documentation. * Improve split panel repeat() syntax, code cleanup. - Move helper methods for split panel (toSnapFunction), and type definitions (SnapFunction, SnapFunctionParams) to split-panel.component.ts - Allow repeat() to exist within other snap points in the split panel snap property. e.g. "repeat(100px) 50% 70% 90px" is now valid. * Apply suggestions from code review Co-authored-by: Cory LaViska <cory@abeautifulsite.net> --------- Co-authored-by: Cory LaViska <cory@abeautifulsite.net>pull/2370/head
rodzic
e3b117dc9a
commit
a7aadc93f9
|
@ -200,20 +200,22 @@ const App = () => (
|
||||||
|
|
||||||
### Snapping
|
### Snapping
|
||||||
|
|
||||||
To snap panels at specific positions while dragging, add the `snap` attribute with one or more space-separated values. Values must be in pixels or percentages. For example, to snap the panel at `100px` and `50%`, use `snap="100px 50%"`. You can also customize how close the divider must be before snapping with the `snap-threshold` attribute.
|
To snap panels at specific positions while dragging, you can use the `snap` attribute. You can provide one or more space-separated pixel or percentage values, either as single values or within a `repeat()` expression, which will be repeated along the length of the panel. You can also customize how close the divider must be before snapping with the `snap-threshold` attribute.
|
||||||
|
|
||||||
|
For example, to snap the panel at `100px` and `50%`, use `snap="100px 50%"`.
|
||||||
|
|
||||||
```html:preview
|
```html:preview
|
||||||
<div class="split-panel-snapping">
|
<div class="split-panel-snapping">
|
||||||
<sl-split-panel snap="100px 50%">
|
<sl-split-panel snap="100px 50%">
|
||||||
<div
|
<div
|
||||||
slot="start"
|
slot="start"
|
||||||
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
|
style="height: 150px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
|
||||||
>
|
>
|
||||||
Start
|
Start
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
slot="end"
|
slot="end"
|
||||||
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
|
style="height: 150px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
|
||||||
>
|
>
|
||||||
End
|
End
|
||||||
</div>
|
</div>
|
||||||
|
@ -239,16 +241,105 @@ To snap panels at specific positions while dragging, add the `snap` attribute wi
|
||||||
transform: translateX(-3px);
|
transform: translateX(-3px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.split-panel-snapping-dots::before {
|
.split-panel-snapping .split-panel-snapping-dots::before {
|
||||||
left: 100px;
|
left: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.split-panel-snapping-dots::after {
|
.split-panel-snapping .split-panel-snapping-dots::after {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or, if you want to snap the panel to every `100px` interval, as well as at 50% of the panel's size, you can use `snap="repeat(100px) 50%"`.
|
||||||
|
|
||||||
|
```html:preview
|
||||||
|
<div class="split-panel-snapping-repeat">
|
||||||
|
<sl-split-panel snap="repeat(100px) 50%">
|
||||||
|
<div
|
||||||
|
slot="start"
|
||||||
|
style="height: 150px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
slot="end"
|
||||||
|
style="height: 150px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
|
||||||
|
>
|
||||||
|
End
|
||||||
|
</div>
|
||||||
|
</sl-split-panel>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.split-panel-snapping-repeat {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using a Custom Snap Function
|
||||||
|
|
||||||
|
You can also implement a custom snap function which controls the snapping manually. To do this, you need to acquire a reference to the element in Javascript and set the `snap` property. For example, if you want to snap the divider to either `100px` from the left or `100px` from the right, you can set the `snap` property to a function encoding that logic.
|
||||||
|
|
||||||
|
```js
|
||||||
|
panel.snap = ({ pos, size }) => (pos < size / 2) ? 100 : (size - 100)
|
||||||
|
|
||||||
|
Note that the `snap-threshold` property will not automatically be applied if `snap` is set to a function. Instead, the function itself must handle applying the threshold if desired, and is passed a `snapThreshold` member with its parameters.
|
||||||
|
|
||||||
|
```html:preview
|
||||||
|
<div class="split-panel-snapping-fn">
|
||||||
|
<sl-split-panel>
|
||||||
|
<div
|
||||||
|
slot="start"
|
||||||
|
style="height: 150px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
slot="end"
|
||||||
|
style="height: 150px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
|
||||||
|
>
|
||||||
|
End
|
||||||
|
</div>
|
||||||
|
</sl-split-panel>
|
||||||
|
|
||||||
|
<div class="split-panel-snapping-dots"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.split-panel-snapping-fn {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-panel-snapping-fn .split-panel-snapping-dots::before,
|
||||||
|
.split-panel-snapping-fn .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-fn .split-panel-snapping-dots::before {
|
||||||
|
left: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-panel-snapping-fn .split-panel-snapping-dots::after {
|
||||||
|
left: calc(100% - 100px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const container = document.querySelector('.split-panel-snapping-fn');
|
||||||
|
const splitPanel = container.querySelector('sl-split-panel');
|
||||||
|
splitPanel.snap = ({ pos, size }) => (pos < size / 2) ? 100 : (size - 100);
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
{% raw %}
|
{% raw %}
|
||||||
|
|
||||||
```jsx:react
|
```jsx:react
|
||||||
|
|
|
@ -10,6 +10,25 @@ import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||||
import styles from './split-panel.styles.js';
|
import styles from './split-panel.styles.js';
|
||||||
import type { CSSResultGroup } from 'lit';
|
import type { CSSResultGroup } from 'lit';
|
||||||
|
|
||||||
|
export interface SnapFunctionParams {
|
||||||
|
/** The position the divider has been dragged to, in pixels. */
|
||||||
|
pos: number;
|
||||||
|
/** The size of the split-panel across its primary axis, in pixels. */
|
||||||
|
size: number;
|
||||||
|
/** The snap-threshold passed to the split-panel, in pixels. May be infinity. */
|
||||||
|
snapThreshold: number;
|
||||||
|
/** Whether or not the user-agent is RTL. */
|
||||||
|
isRtl: boolean;
|
||||||
|
/** Whether or not the split panel is vertical. */
|
||||||
|
vertical: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Used by sl-split-panel to convert an input position into a snapped position. */
|
||||||
|
export type SnapFunction = (opt: SnapFunctionParams) => number | null;
|
||||||
|
|
||||||
|
/** A SnapFunction which performs no snapping. */
|
||||||
|
export const SNAP_NONE = () => null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Split panels display two adjacent panels, allowing the user to reposition them.
|
* @summary Split panels display two adjacent panels, allowing the user to reposition them.
|
||||||
* @documentation https://shoelace.style/components/split-panel
|
* @documentation https://shoelace.style/components/split-panel
|
||||||
|
@ -67,11 +86,73 @@ export default class SlSplitPanel extends ShoelaceElement {
|
||||||
*/
|
*/
|
||||||
@property() primary?: 'start' | 'end';
|
@property() primary?: 'start' | 'end';
|
||||||
|
|
||||||
|
// Returned when the property is queried, so that string 'snap's are preserved.
|
||||||
|
private snapValue: string | SnapFunction = '';
|
||||||
|
// Actually used for computing snap points. All string snaps are converted via `toSnapFunction`.
|
||||||
|
private snapFunction: SnapFunction = SNAP_NONE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One or more space-separated values at which the divider should snap. Values can be in pixels or percentages, e.g.
|
* Converts a string containing either a series of fixed/repeated snap points (e.g. "repeat(20%)", "100px 200px 800px", or "10% 50% repeat(10px)") into a SnapFunction. `SnapFunction`s take in a `SnapFunctionOpts` and return the position that the split panel should snap to.
|
||||||
* `"100px 50%"`.
|
*
|
||||||
|
* @param snap - The snap string.
|
||||||
|
* @returns a `SnapFunction` representing the snap string's logic.
|
||||||
*/
|
*/
|
||||||
@property() snap?: string;
|
private toSnapFunction(snap: string): SnapFunction {
|
||||||
|
const snapPoints = snap.split(" ");
|
||||||
|
|
||||||
|
return ({ pos, size, snapThreshold, isRtl, vertical }) => {
|
||||||
|
let newPos = pos;
|
||||||
|
let minDistance = Number.POSITIVE_INFINITY;
|
||||||
|
|
||||||
|
snapPoints.forEach(value => {
|
||||||
|
let snapPoint: number;
|
||||||
|
|
||||||
|
if (value.startsWith("repeat(")) {
|
||||||
|
const repeatVal = snap.substring("repeat(".length, snap.length - 1);
|
||||||
|
const isPercent = repeatVal.endsWith("%");
|
||||||
|
const repeatNum = Number.parseFloat(repeatVal);
|
||||||
|
const snapIntervalPx = isPercent ? size * (repeatNum / 100) : repeatNum;
|
||||||
|
snapPoint = Math.round((isRtl && !vertical ? size - pos : pos) / snapIntervalPx) * snapIntervalPx;
|
||||||
|
}
|
||||||
|
else if (value.endsWith("%")) {
|
||||||
|
snapPoint = size * (Number.parseFloat(value) / 100);
|
||||||
|
} else {
|
||||||
|
snapPoint = Number.parseFloat(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRtl && !vertical) {
|
||||||
|
snapPoint = size - snapPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
const distance = Math.abs(pos - snapPoint);
|
||||||
|
|
||||||
|
if (distance <= snapThreshold && distance < minDistance) {
|
||||||
|
newPos = snapPoint;
|
||||||
|
minDistance = distance;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return newPos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Either one or more space-separated values at which the divider should snap, in pixels, percentages, or repeat expressions e.g. `'100px 50% 500px' or `repeat(50%) 10px`,
|
||||||
|
* or a function which takes in a `SnapFunctionParams`, and returns a position to snap to, e.g. `({ pos }) => Math.round(pos / 8) * 8`.
|
||||||
|
*/
|
||||||
|
@property({ reflect: true })
|
||||||
|
set snap(snap: string | SnapFunction | null | undefined) {
|
||||||
|
this.snapValue = snap ?? ''
|
||||||
|
if (snap) {
|
||||||
|
this.snapFunction = typeof snap === 'string' ? this.toSnapFunction(snap) : snap;
|
||||||
|
} else {
|
||||||
|
this.snapFunction = SNAP_NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get snap(): string | SnapFunction {
|
||||||
|
return this.snapValue;
|
||||||
|
}
|
||||||
|
|
||||||
/** How close the divider must be to a snap point until snapping occurs. */
|
/** How close the divider must be to a snap point until snapping occurs. */
|
||||||
@property({ type: Number, attribute: 'snap-threshold' }) snapThreshold = 12;
|
@property({ type: Number, attribute: 'snap-threshold' }) snapThreshold = 12;
|
||||||
|
@ -125,30 +206,14 @@ export default class SlSplitPanel extends ShoelaceElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check snap points
|
// Check snap points
|
||||||
if (this.snap) {
|
newPositionInPixels =
|
||||||
const snaps = this.snap.split(' ');
|
this.snapFunction({
|
||||||
|
pos: newPositionInPixels,
|
||||||
snaps.forEach(value => {
|
size: this.size,
|
||||||
let snapPoint: number;
|
snapThreshold: this.snapThreshold,
|
||||||
|
isRtl: isRtl,
|
||||||
if (value.endsWith('%')) {
|
vertical: this.vertical
|
||||||
snapPoint = this.size * (parseFloat(value) / 100);
|
}) ?? newPositionInPixels;
|
||||||
} else {
|
|
||||||
snapPoint = parseFloat(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRtl && !this.vertical) {
|
|
||||||
snapPoint = this.size - snapPoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
newPositionInPixels >= snapPoint - this.snapThreshold &&
|
|
||||||
newPositionInPixels <= snapPoint + this.snapThreshold
|
|
||||||
) {
|
|
||||||
newPositionInPixels = snapPoint;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.position = clamp(this.pixelsToPercentage(newPositionInPixels), 0, 100);
|
this.position = clamp(this.pixelsToPercentage(newPositionInPixels), 0, 100);
|
||||||
},
|
},
|
||||||
|
|
Ładowanie…
Reference in New Issue