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
|
||||
|
||||
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
|
||||
<div class="split-panel-snapping">
|
||||
<sl-split-panel snap="100px 50%">
|
||||
<div
|
||||
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
|
||||
</div>
|
||||
<div
|
||||
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
|
||||
</div>
|
||||
|
@ -239,16 +241,105 @@ To snap panels at specific positions while dragging, add the `snap` attribute wi
|
|||
transform: translateX(-3px);
|
||||
}
|
||||
|
||||
.split-panel-snapping-dots::before {
|
||||
.split-panel-snapping .split-panel-snapping-dots::before {
|
||||
left: 100px;
|
||||
}
|
||||
|
||||
.split-panel-snapping-dots::after {
|
||||
.split-panel-snapping .split-panel-snapping-dots::after {
|
||||
left: 50%;
|
||||
}
|
||||
</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 %}
|
||||
|
||||
```jsx:react
|
||||
|
|
|
@ -10,6 +10,25 @@ import ShoelaceElement from '../../internal/shoelace-element.js';
|
|||
import styles from './split-panel.styles.js';
|
||||
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.
|
||||
* @documentation https://shoelace.style/components/split-panel
|
||||
|
@ -67,11 +86,73 @@ export default class SlSplitPanel extends ShoelaceElement {
|
|||
*/
|
||||
@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.
|
||||
* `"100px 50%"`.
|
||||
* 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.
|
||||
*
|
||||
* @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. */
|
||||
@property({ type: Number, attribute: 'snap-threshold' }) snapThreshold = 12;
|
||||
|
@ -125,30 +206,14 @@ export default class SlSplitPanel extends ShoelaceElement {
|
|||
}
|
||||
|
||||
// Check snap points
|
||||
if (this.snap) {
|
||||
const snaps = this.snap.split(' ');
|
||||
|
||||
snaps.forEach(value => {
|
||||
let snapPoint: number;
|
||||
|
||||
if (value.endsWith('%')) {
|
||||
snapPoint = this.size * (parseFloat(value) / 100);
|
||||
} else {
|
||||
snapPoint = parseFloat(value);
|
||||
}
|
||||
|
||||
if (isRtl && !this.vertical) {
|
||||
snapPoint = this.size - snapPoint;
|
||||
}
|
||||
|
||||
if (
|
||||
newPositionInPixels >= snapPoint - this.snapThreshold &&
|
||||
newPositionInPixels <= snapPoint + this.snapThreshold
|
||||
) {
|
||||
newPositionInPixels = snapPoint;
|
||||
}
|
||||
});
|
||||
}
|
||||
newPositionInPixels =
|
||||
this.snapFunction({
|
||||
pos: newPositionInPixels,
|
||||
size: this.size,
|
||||
snapThreshold: this.snapThreshold,
|
||||
isRtl: isRtl,
|
||||
vertical: this.vertical
|
||||
}) ?? newPositionInPixels;
|
||||
|
||||
this.position = clamp(this.pixelsToPercentage(newPositionInPixels), 0, 100);
|
||||
},
|
||||
|
|
Ładowanie…
Reference in New Issue