Improve routing rule drag handling and controls
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
export const HARBOR_VERSIONS = Object.freeze({
|
export const HARBOR_VERSIONS = Object.freeze({
|
||||||
macClient: '0.26.2',
|
macClient: '0.26.3',
|
||||||
gatewayClient: '0.27.2',
|
gatewayClient: '0.27.3',
|
||||||
gatewayBackend: '0.27.0',
|
gatewayBackend: '0.27.0',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ interface RuleDragSession extends RuleReorderLifecycle {
|
|||||||
pointerOffsetY: number;
|
pointerOffsetY: number;
|
||||||
translateY: number;
|
translateY: number;
|
||||||
raf: number | null;
|
raf: number | null;
|
||||||
|
capture: HTMLElement;
|
||||||
handle: HTMLButtonElement;
|
handle: HTMLButtonElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,9 +448,10 @@ export function useRoutingFeature({
|
|||||||
const lifecycle = beginRuleReorder(dragRef.current, ruleKey, 'pointer');
|
const lifecycle = beginRuleReorder(dragRef.current, ruleKey, 'pointer');
|
||||||
if (!lifecycle) return;
|
if (!lifecycle) return;
|
||||||
const row = event.currentTarget.closest<HTMLElement>('[data-rule-key]');
|
const row = event.currentTarget.closest<HTMLElement>('[data-rule-key]');
|
||||||
if (!row) return;
|
const capture = event.currentTarget.closest<HTMLElement>('.client-local-rules-list');
|
||||||
|
if (!row || !capture) return;
|
||||||
const rect = row.getBoundingClientRect();
|
const rect = row.getBoundingClientRect();
|
||||||
event.currentTarget.setPointerCapture(event.pointerId);
|
capture.setPointerCapture(event.pointerId);
|
||||||
dragRef.current = {
|
dragRef.current = {
|
||||||
...lifecycle,
|
...lifecycle,
|
||||||
originRules: [...rulesRef.current],
|
originRules: [...rulesRef.current],
|
||||||
@@ -459,11 +461,12 @@ export function useRoutingFeature({
|
|||||||
pointerOffsetY: event.clientY - (rect.top + rect.height / 2),
|
pointerOffsetY: event.clientY - (rect.top + rect.height / 2),
|
||||||
translateY: 0,
|
translateY: 0,
|
||||||
raf: null,
|
raf: null,
|
||||||
|
capture,
|
||||||
handle: event.currentTarget,
|
handle: event.currentTarget,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function movePointerReorder(event: ReactPointerEvent<HTMLButtonElement>) {
|
function movePointerReorder(event: ReactPointerEvent<HTMLElement>) {
|
||||||
const session = dragRef.current;
|
const session = dragRef.current;
|
||||||
if (!session || session.pointerId !== event.pointerId) return;
|
if (!session || session.pointerId !== event.pointerId) return;
|
||||||
session.latestY = event.clientY;
|
session.latestY = event.clientY;
|
||||||
@@ -514,9 +517,9 @@ export function useRoutingFeature({
|
|||||||
if (
|
if (
|
||||||
outcome.releasePointerCapture
|
outcome.releasePointerCapture
|
||||||
&& session.pointerId !== null
|
&& session.pointerId !== null
|
||||||
&& session.handle.hasPointerCapture(session.pointerId)
|
&& session.capture.hasPointerCapture(session.pointerId)
|
||||||
) {
|
) {
|
||||||
session.handle.releasePointerCapture(session.pointerId);
|
session.capture.releasePointerCapture(session.pointerId);
|
||||||
}
|
}
|
||||||
if (session.lifted) {
|
if (session.lifted) {
|
||||||
if (reason === 'drop' && session.input === 'pointer') suppressHandleClickRef.current = true;
|
if (reason === 'drop' && session.input === 'pointer') suppressHandleClickRef.current = true;
|
||||||
@@ -536,17 +539,17 @@ export function useRoutingFeature({
|
|||||||
return finishReorder('cancel', announce);
|
return finishReorder('cancel', announce);
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishPointerReorder(event: ReactPointerEvent<HTMLButtonElement>) {
|
function finishPointerReorder(event: ReactPointerEvent<HTMLElement>) {
|
||||||
if (dragRef.current?.pointerId !== event.pointerId) return;
|
if (dragRef.current?.pointerId !== event.pointerId) return;
|
||||||
dropReorder();
|
dropReorder();
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelPointerReorder(event: ReactPointerEvent<HTMLButtonElement>) {
|
function cancelPointerReorder(event: ReactPointerEvent<HTMLElement>) {
|
||||||
if (dragRef.current?.pointerId !== event.pointerId) return;
|
if (dragRef.current?.pointerId !== event.pointerId) return;
|
||||||
cancelReorder();
|
cancelReorder();
|
||||||
}
|
}
|
||||||
|
|
||||||
function losePointerReorder(event: ReactPointerEvent<HTMLButtonElement>) {
|
function losePointerReorder(event: ReactPointerEvent<HTMLElement>) {
|
||||||
if (dragRef.current?.pointerId !== event.pointerId) return;
|
if (dragRef.current?.pointerId !== event.pointerId) return;
|
||||||
finishReorder('lost-capture');
|
finishReorder('lost-capture');
|
||||||
}
|
}
|
||||||
@@ -569,6 +572,7 @@ export function useRoutingFeature({
|
|||||||
pointerOffsetY: 0,
|
pointerOffsetY: 0,
|
||||||
translateY: 0,
|
translateY: 0,
|
||||||
raf: null,
|
raf: null,
|
||||||
|
capture: handle,
|
||||||
handle,
|
handle,
|
||||||
};
|
};
|
||||||
setLiftedKey(ruleKey);
|
setLiftedKey(ruleKey);
|
||||||
@@ -929,6 +933,10 @@ export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature;
|
|||||||
className={`client-local-rules-list${draftRules.length > 1 ? ' has-order-flow' : ''}`}
|
className={`client-local-rules-list${draftRules.length > 1 ? ' has-order-flow' : ''}`}
|
||||||
role="list"
|
role="list"
|
||||||
aria-label="Правила применяются сверху вниз"
|
aria-label="Правила применяются сверху вниз"
|
||||||
|
onPointerMove={feature.movePointerReorder}
|
||||||
|
onPointerUp={feature.finishPointerReorder}
|
||||||
|
onPointerCancel={feature.cancelPointerReorder}
|
||||||
|
onLostPointerCapture={feature.losePointerReorder}
|
||||||
>
|
>
|
||||||
{feature.rules.map((rule, index) => {
|
{feature.rules.map((rule, index) => {
|
||||||
const [status, statusLabel] = localRuleStatus(
|
const [status, statusLabel] = localRuleStatus(
|
||||||
@@ -959,18 +967,19 @@ export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature;
|
|||||||
aria-pressed={lifted}
|
aria-pressed={lifted}
|
||||||
disabled={handleDisabled}
|
disabled={handleDisabled}
|
||||||
onPointerDown={(event) => feature.startPointerReorder(event, rule._key)}
|
onPointerDown={(event) => feature.startPointerReorder(event, rule._key)}
|
||||||
onPointerMove={feature.movePointerReorder}
|
|
||||||
onPointerUp={feature.finishPointerReorder}
|
|
||||||
onPointerCancel={feature.cancelPointerReorder}
|
|
||||||
onLostPointerCapture={feature.losePointerReorder}
|
|
||||||
onKeyDown={(event) => feature.handleReorderKey(event, rule._key)}
|
onKeyDown={(event) => feature.handleReorderKey(event, rule._key)}
|
||||||
onClick={(event) => feature.handleReorderClick(event, rule._key)}
|
onClick={(event) => feature.handleReorderClick(event, rule._key)}
|
||||||
onBlur={(event) => feature.handleReorderBlur(event, rule._key)}
|
onBlur={(event) => feature.handleReorderBlur(event, rule._key)}
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 12 28" aria-hidden="true">
|
<svg viewBox="0 0 16 28" aria-hidden="true">
|
||||||
<circle cx="6" cy="6" r="1.6" />
|
<circle cx="5" cy="5" r="1.25" />
|
||||||
<circle cx="6" cy="14" r="1.6" />
|
<circle cx="11" cy="5" r="1.25" />
|
||||||
<circle cx="6" cy="22" r="1.6" />
|
<circle cx="5" cy="11" r="1.25" />
|
||||||
|
<circle cx="11" cy="11" r="1.25" />
|
||||||
|
<circle cx="5" cy="17" r="1.25" />
|
||||||
|
<circle cx="11" cy="17" r="1.25" />
|
||||||
|
<circle cx="5" cy="23" r="1.25" />
|
||||||
|
<circle cx="11" cy="23" r="1.25" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<span className="client-local-rule-enabled-wrap client-tooltip-anchor">
|
<span className="client-local-rule-enabled-wrap client-tooltip-anchor">
|
||||||
@@ -984,11 +993,15 @@ export function RoutingPanel({ feature, statusSlot }: { feature: RoutingFeature;
|
|||||||
onClick={() => feature.change(index, 'enabled', !rule.enabled)}
|
onClick={() => feature.change(index, 'enabled', !rule.enabled)}
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 20 20" aria-hidden="true">
|
<svg viewBox="0 0 20 20" aria-hidden="true">
|
||||||
<rect className="client-rule-switch-track" x="2" y="6" width="16" height="8" rx="4" />
|
<circle cx="10" cy="10" r="6" />
|
||||||
<circle className="client-rule-switch-thumb" cx="6" cy="10" r="2.5" />
|
<path className="client-rule-check" d="m6.8 10.1 2.1 2.2 4.5-5" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<Tooltip>{rule.enabled ? 'Отключить правило' : 'Включить правило'}</Tooltip>
|
<Tooltip>
|
||||||
|
{rule.enabled
|
||||||
|
? 'Отключить: правило перестанет участвовать в маршрутизации'
|
||||||
|
: 'Включить: правило снова будет участвовать в маршрутизации'}
|
||||||
|
</Tooltip>
|
||||||
</span>
|
</span>
|
||||||
<RuleTypePicker
|
<RuleTypePicker
|
||||||
value={rule.type}
|
value={rule.type}
|
||||||
|
|||||||
@@ -111,7 +111,8 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
column-gap: 6px;
|
column-gap: 6px;
|
||||||
padding: 4px 0;
|
padding: 4px 0;
|
||||||
background: linear-gradient(90deg, color-mix(in oklch, var(--client-rule-route-color) 8%, transparent), transparent 58%);
|
border-radius: 10px;
|
||||||
|
background: color-mix(in oklch, var(--client-rule-route-color) 6%, transparent);
|
||||||
animation: client-row-enter 560ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
animation: client-row-enter 560ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
transition: background 320ms ease, box-shadow 180ms ease, opacity 260ms ease, filter 360ms ease;
|
transition: background 320ms ease, box-shadow 180ms ease, opacity 260ms ease, filter 360ms ease;
|
||||||
}
|
}
|
||||||
@@ -120,21 +121,6 @@
|
|||||||
--client-rule-route-color: light-dark(oklch(0.52 0.11 240), oklch(0.76 0.1 240));
|
--client-rule-route-color: light-dark(oklch(0.52 0.11 240), oklch(0.76 0.1 240));
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rule::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
bottom: 10px;
|
|
||||||
left: 0;
|
|
||||||
width: 2px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: var(--client-rule-route-color);
|
|
||||||
box-shadow: 0 0 7px color-mix(in oklch, var(--client-rule-route-color) 44%, transparent);
|
|
||||||
opacity: 0.78;
|
|
||||||
pointer-events: none;
|
|
||||||
transition: background 320ms ease, box-shadow 320ms ease, opacity 260ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-local-rule + .client-local-rule::before {
|
.client-local-rule + .client-local-rule::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -208,15 +194,10 @@
|
|||||||
filter: saturate(0);
|
filter: saturate(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rule.is-disabled::after {
|
|
||||||
opacity: 0.22;
|
|
||||||
filter: saturate(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-local-rule.is-dragging {
|
.client-local-rule.is-dragging {
|
||||||
z-index: 6;
|
z-index: 6;
|
||||||
animation: none;
|
animation: none;
|
||||||
background: linear-gradient(90deg, color-mix(in oklch, var(--client-rule-route-color) 13%, transparent), transparent 72%);
|
background: color-mix(in oklch, var(--client-rule-route-color) 11%, transparent);
|
||||||
box-shadow: 0 10px 28px color-mix(in oklch, var(--client-rule-route-color) 16%, transparent);
|
box-shadow: 0 10px 28px color-mix(in oklch, var(--client-rule-route-color) 16%, transparent);
|
||||||
transform: translateY(var(--client-rule-drag-y));
|
transform: translateY(var(--client-rule-drag-y));
|
||||||
}
|
}
|
||||||
@@ -260,10 +241,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-rule-handle svg {
|
.client-rule-handle svg {
|
||||||
width: 12px;
|
width: 18px;
|
||||||
height: 28px;
|
height: 32px;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
padding: 2px 1px;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: color-mix(in oklch, currentColor 6%, transparent);
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
|
transition: background 220ms ease, transform 220ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-rule-handle:hover:not(:disabled),
|
.client-rule-handle:hover:not(:disabled),
|
||||||
@@ -278,32 +263,39 @@
|
|||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.client-rule-handle:hover:not(:disabled) svg,
|
||||||
|
.client-rule-handle:focus-visible svg,
|
||||||
|
.client-rule-handle[aria-pressed='true'] svg {
|
||||||
|
background: color-mix(in oklch, currentColor 12%, transparent);
|
||||||
|
transform: scale(1.04);
|
||||||
|
}
|
||||||
|
|
||||||
.client-rule-handle:disabled {
|
.client-rule-handle:disabled {
|
||||||
opacity: 0.28;
|
opacity: 0.28;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rule-enabled svg {
|
.client-local-rule-enabled svg {
|
||||||
width: 20px;
|
width: 18px;
|
||||||
height: 20px;
|
height: 18px;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
fill: none;
|
fill: none;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
transition: color 260ms ease, filter 360ms ease, transform 420ms cubic-bezier(0.16, 1, 0.3, 1);
|
transition: color 260ms ease, filter 360ms ease, transform 420ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-rule-switch-track {
|
.client-local-rule-enabled circle {
|
||||||
fill: color-mix(in oklch, var(--client-muted) 16%, transparent);
|
stroke-width: 1.4;
|
||||||
stroke: currentColor;
|
transition: fill 320ms ease, stroke 260ms ease;
|
||||||
stroke-width: 1;
|
|
||||||
transition: fill 260ms ease, stroke 260ms ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-rule-switch-thumb {
|
.client-rule-check {
|
||||||
fill: currentColor;
|
stroke-width: 1.8;
|
||||||
stroke: none;
|
stroke-dasharray: 12;
|
||||||
transform-box: fill-box;
|
stroke-dashoffset: 12;
|
||||||
transform-origin: center;
|
transition: opacity 160ms ease, stroke-dashoffset 360ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
transition: transform 320ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rule-enabled[aria-checked='true'] {
|
.client-local-rule-enabled[aria-checked='true'] {
|
||||||
@@ -311,15 +303,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rule-enabled[aria-checked='true'] svg {
|
.client-local-rule-enabled[aria-checked='true'] svg {
|
||||||
filter: drop-shadow(0 0 7px color-mix(in oklch, var(--client-rule-route-color) 42%, transparent));
|
filter: drop-shadow(0 0 7px color-mix(in oklch, var(--client-rule-route-color) 48%, transparent));
|
||||||
|
transform: scale(1.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rule-enabled[aria-checked='true'] .client-rule-switch-track {
|
.client-local-rule-enabled[aria-checked='true'] circle {
|
||||||
fill: color-mix(in oklch, var(--client-rule-route-color) 20%, transparent);
|
fill: color-mix(in oklch, var(--client-rule-route-color) 12%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rule-enabled[aria-checked='true'] .client-rule-switch-thumb {
|
.client-local-rule-enabled[aria-checked='true'] .client-rule-check {
|
||||||
transform: translateX(8px);
|
stroke-dashoffset: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-rule-type {
|
.client-rule-type {
|
||||||
|
|||||||
@@ -287,11 +287,12 @@
|
|||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-deletable-row.is-removing > :not(.client-delete-strike) {
|
.client-deletable-row.is-removing > :not(.client-delete-strike):not(.client-local-rule-enabled-wrap) {
|
||||||
animation: client-delete-content-dim 820ms ease-out both;
|
animation: client-delete-content-dim 820ms ease-out both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-deletable-row.is-removing .client-rule-switch-thumb {
|
.client-deletable-row.is-removing .client-local-rule-enabled circle,
|
||||||
|
.client-deletable-row.is-removing .client-rule-check {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,15 +302,15 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--client-muted);
|
color: oklch(0.62 0.16 28);
|
||||||
font-size: var(--font-size-icon-delete);
|
font-size: var(--font-size-icon-delete);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 180ms ease, filter 260ms ease, transform 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
transition: color 180ms ease, filter 260ms ease, transform 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-row-delete:hover {
|
.client-row-delete:hover {
|
||||||
color: oklch(0.68 0.15 28);
|
color: oklch(0.7 0.18 28);
|
||||||
filter: drop-shadow(0 0 7px oklch(0.68 0.15 28 / 0.42));
|
filter: drop-shadow(0 0 7px oklch(0.7 0.18 28 / 0.42));
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-row-delete:hover:not(:has(svg)) {
|
.client-row-delete:hover:not(:has(svg)) {
|
||||||
@@ -341,7 +342,6 @@
|
|||||||
.client-local-rule-enabled:focus-visible,
|
.client-local-rule-enabled:focus-visible,
|
||||||
.client-rule-type-trigger:focus-visible,
|
.client-rule-type-trigger:focus-visible,
|
||||||
.client-rule-outbound button:focus-visible,
|
.client-rule-outbound button:focus-visible,
|
||||||
.client-row-delete:focus-visible,
|
|
||||||
.client-row-add:focus-visible,
|
.client-row-add:focus-visible,
|
||||||
.client-local-rules-save:focus-visible,
|
.client-local-rules-save:focus-visible,
|
||||||
.client-local-rules-actions button:focus-visible {
|
.client-local-rules-actions button:focus-visible {
|
||||||
@@ -350,6 +350,12 @@
|
|||||||
text-shadow: 0 0 10px color-mix(in oklch, var(--client-accent) 48%, transparent);
|
text-shadow: 0 0 10px color-mix(in oklch, var(--client-accent) 48%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.client-row-delete:focus-visible {
|
||||||
|
outline: 0;
|
||||||
|
color: oklch(0.7 0.18 28);
|
||||||
|
filter: drop-shadow(0 0 7px oklch(0.7 0.18 28 / 0.42));
|
||||||
|
}
|
||||||
|
|
||||||
.client-row-add {
|
.client-row-add {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
padding: 7px 0;
|
padding: 7px 0;
|
||||||
|
|||||||
@@ -122,11 +122,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.client-local-rule,
|
.client-local-rule,
|
||||||
.client-local-rule::after,
|
|
||||||
.client-rule-handle,
|
.client-rule-handle,
|
||||||
|
.client-rule-handle svg,
|
||||||
.client-local-rule-enabled svg,
|
.client-local-rule-enabled svg,
|
||||||
.client-rule-switch-track,
|
.client-local-rule-enabled circle,
|
||||||
.client-rule-switch-thumb,
|
.client-local-rule-enabled path,
|
||||||
.client-rule-type-trigger,
|
.client-rule-type-trigger,
|
||||||
.client-rule-type-trigger svg,
|
.client-rule-type-trigger svg,
|
||||||
.client-rule-type-list,
|
.client-rule-type-list,
|
||||||
|
|||||||
@@ -37,13 +37,16 @@ test('routing controller owns ordered outbound drafts, capability gating and dra
|
|||||||
assert.match(feature, /if \(!editable \|\| blocked\) return/);
|
assert.match(feature, /if \(!editable \|\| blocked\) return/);
|
||||||
assert.match(feature, /sameRule\(rule, savedRules\[index\]\)/);
|
assert.match(feature, /sameRule\(rule, savedRules\[index\]\)/);
|
||||||
assert.match(feature, /sameRule\(rule, activeRules\[index\]\)/);
|
assert.match(feature, /sameRule\(rule, activeRules\[index\]\)/);
|
||||||
assert.match(feature, /beginRuleReorder\(dragRef\.current, ruleKey, 'pointer'\)[\s\S]*setPointerCapture/);
|
assert.match(feature, /beginRuleReorder\(dragRef\.current, ruleKey, 'pointer'\)[\s\S]*capture\.setPointerCapture/);
|
||||||
|
assert.match(feature, /const capture = event\.currentTarget\.closest<HTMLElement>\('\.client-local-rules-list'\)/);
|
||||||
|
assert.doesNotMatch(feature, /event\.currentTarget\.setPointerCapture/);
|
||||||
assert.match(feature, /beginRuleReorder\(dragRef\.current, ruleKey, 'keyboard'\)/);
|
assert.match(feature, /beginRuleReorder\(dragRef\.current, ruleKey, 'keyboard'\)/);
|
||||||
assert.match(feature, /event\.detail === 0\) toggleKeyboardReorder/);
|
assert.match(feature, /event\.detail === 0\) toggleKeyboardReorder/);
|
||||||
assert.match(feature, /event\.key === 'Tab'[\s\S]*finishReorder\('focus-leave'\)/);
|
assert.match(feature, /event\.key === 'Tab'[\s\S]*finishReorder\('focus-leave'\)/);
|
||||||
assert.match(feature, /onBlur=\{\(event\) => feature\.handleReorderBlur/);
|
assert.match(feature, /onBlur=\{\(event\) => feature\.handleReorderBlur/);
|
||||||
assert.match(feature, /endRuleReorder\(session, 'unmount'\)\.stopAutoScroll/);
|
assert.match(feature, /endRuleReorder\(session, 'unmount'\)\.stopAutoScroll/);
|
||||||
assert.match(feature, /onLostPointerCapture=\{feature\.losePointerReorder\}/);
|
assert.match(feature, /className=\{`client-local-rules-list[\s\S]*onPointerMove=\{feature\.movePointerReorder\}[\s\S]*onLostPointerCapture=\{feature\.losePointerReorder\}/);
|
||||||
|
assert.match(feature, /session\.capture\.hasPointerCapture[\s\S]*session\.capture\.releasePointerCapture/);
|
||||||
assert.match(feature, /keyboardEvent\.preventDefault\(\);[\s\S]*cancelReorder\(\)/);
|
assert.match(feature, /keyboardEvent\.preventDefault\(\);[\s\S]*cancelReorder\(\)/);
|
||||||
assert.match(feature, /stopAutoScroll\(session\)[\s\S]*Перемещение отменено/);
|
assert.match(feature, /stopAutoScroll\(session\)[\s\S]*Перемещение отменено/);
|
||||||
assert.match(feature, /const slotCenter = \(element: HTMLElement\) => listTop \+ element\.offsetTop \+ element\.offsetHeight \/ 2/);
|
assert.match(feature, /const slotCenter = \(element: HTMLElement\) => listTop \+ element\.offsetTop \+ element\.offsetHeight \/ 2/);
|
||||||
|
|||||||
@@ -102,12 +102,14 @@ test('copy feedback, drawers and Gateway access actions expose complete semantic
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('ordered rules use one accessible drag handle and a compact icon route control', () => {
|
test('ordered rules use one accessible drag handle and a compact icon route control', () => {
|
||||||
|
const handle = /<button\s+className="client-rule-handle"([\s\S]*?)<\/button>/.exec(routing)?.[0] || '';
|
||||||
assert.match(routing, /className="client-rule-handle"[\s\S]*type="button"[\s\S]*aria-label=\{`Переместить правило, позиция/);
|
assert.match(routing, /className="client-rule-handle"[\s\S]*type="button"[\s\S]*aria-label=\{`Переместить правило, позиция/);
|
||||||
assert.match(routing, /aria-describedby="client-rule-reorder-instructions"[\s\S]*aria-pressed=\{lifted\}/);
|
assert.match(routing, /aria-describedby="client-rule-reorder-instructions"[\s\S]*aria-pressed=\{lifted\}/);
|
||||||
assert.match(routing, /onPointerDown=\{\(event\) => feature\.startPointerReorder\(event, rule\._key\)\}/);
|
assert.match(routing, /onPointerDown=\{\(event\) => feature\.startPointerReorder\(event, rule\._key\)\}/);
|
||||||
|
assert.doesNotMatch(handle, /onPointerMove|onPointerUp|onPointerCancel|onLostPointerCapture/);
|
||||||
assert.doesNotMatch(routing, /data-rule-key[^>]*onPointerDown/);
|
assert.doesNotMatch(routing, /data-rule-key[^>]*onPointerDown/);
|
||||||
assert.match(routing, /onClick=\{\(event\) => feature\.handleReorderClick\(event, rule\._key\)\}/);
|
assert.match(routing, /onClick=\{\(event\) => feature\.handleReorderClick\(event, rule\._key\)\}/);
|
||||||
assert.match(routing, /<svg viewBox="0 0 12 28" aria-hidden="true">[\s\S]*<circle[\s\S]*<circle[\s\S]*<circle/);
|
assert.match(routing, /<svg viewBox="0 0 16 28" aria-hidden="true">[\s\S]*cx="5" cy="5"[\s\S]*cx="11" cy="23"/);
|
||||||
assert.match(routing, /client-rule-reorder-instructions[\s\S]*стрелки вверх и вниз[\s\S]*Escape отменяет/);
|
assert.match(routing, /client-rule-reorder-instructions[\s\S]*стрелки вверх и вниз[\s\S]*Escape отменяет/);
|
||||||
assert.match(routing, /client-rule-reorder-live[\s\S]*aria-live="polite"/);
|
assert.match(routing, /client-rule-reorder-live[\s\S]*aria-live="polite"/);
|
||||||
assert.match(routing, /className="client-rule-outbound client-tooltip-anchor"[\s\S]*className=\{direct \? 'is-direct' : 'is-vpn'\}/);
|
assert.match(routing, /className="client-rule-outbound client-tooltip-anchor"[\s\S]*className=\{direct \? 'is-direct' : 'is-vpn'\}/);
|
||||||
@@ -116,16 +118,17 @@ test('ordered rules use one accessible drag handle and a compact icon route cont
|
|||||||
assert.doesNotMatch(routing, /client-rule-outbound" role="group"/);
|
assert.doesNotMatch(routing, /client-rule-outbound" role="group"/);
|
||||||
assert.match(routing, /className=\{`client-local-rules-list\$\{draftRules\.length > 1 \? ' has-order-flow' : ''\}`\}[\s\S]*aria-label="Правила применяются сверху вниз"/);
|
assert.match(routing, /className=\{`client-local-rules-list\$\{draftRules\.length > 1 \? ' has-order-flow' : ''\}`\}[\s\S]*aria-label="Правила применяются сверху вниз"/);
|
||||||
assert.match(routing, /className="client-rule-status-dot"/);
|
assert.match(routing, /className="client-rule-status-dot"/);
|
||||||
assert.match(routing, /role="switch"[\s\S]*aria-checked=\{rule\.enabled\}[\s\S]*client-rule-switch-track[\s\S]*client-rule-switch-thumb/);
|
assert.match(routing, /role="switch"[\s\S]*aria-checked=\{rule\.enabled\}[\s\S]*client-rule-check/);
|
||||||
assert.match(routing, /Отключить правило'[\s\S]*Включить правило'[\s\S]*<Tooltip>/);
|
assert.match(routing, /Отключить: правило перестанет участвовать в маршрутизации[\s\S]*Включить: правило снова будет участвовать в маршрутизации/);
|
||||||
assert.match(routing, /data-route=\{rule\.outbound\}/);
|
assert.match(routing, /data-route=\{rule\.outbound\}/);
|
||||||
assert.match(styles, /\.client-local-rule\[data-route='direct'\] \{[\s\S]*--client-rule-route-color/);
|
assert.match(styles, /\.client-local-rule\[data-route='direct'\] \{[\s\S]*--client-rule-route-color/);
|
||||||
assert.match(styles, /\.client-local-rule::after \{[\s\S]*background: var\(--client-rule-route-color\)/);
|
assert.doesNotMatch(styles, /\.client-local-rule::after/);
|
||||||
assert.match(styles, /\.client-local-rule\.is-active \.client-rule-status-dot \{[\s\S]*background: currentColor/);
|
assert.match(styles, /\.client-local-rule\.is-active \.client-rule-status-dot \{[\s\S]*background: currentColor/);
|
||||||
assert.match(styles, /\.client-local-rule\.is-unsaved \.client-rule-status-dot \{[\s\S]*border-radius: 1px[\s\S]*rotate\(45deg\)/);
|
assert.match(styles, /\.client-local-rule\.is-unsaved \.client-rule-status-dot \{[\s\S]*border-radius: 1px[\s\S]*rotate\(45deg\)/);
|
||||||
assert.match(styles, /\.client-local-rule\.is-pending \.client-rule-status-dot \{[\s\S]*border: 1\.5px dashed/);
|
assert.match(styles, /\.client-local-rule\.is-pending \.client-rule-status-dot \{[\s\S]*border: 1\.5px dashed/);
|
||||||
assert.doesNotMatch(routing, />\s*[↑↓]\s*</);
|
assert.doesNotMatch(routing, />\s*[↑↓]\s*</);
|
||||||
assert.match(styles, /\.client-rule-handle \{[\s\S]*width: 44px;[\s\S]*height: 44px;[\s\S]*touch-action: none/);
|
assert.match(styles, /\.client-rule-handle \{[\s\S]*width: 44px;[\s\S]*height: 44px;[\s\S]*touch-action: none/);
|
||||||
|
assert.match(styles, /\.client-row-delete \{[\s\S]*color: oklch\(0\.62 0\.16 28\)/);
|
||||||
assert.match(styles, /\.client-rule-outbound \{[\s\S]*width: 34px/);
|
assert.match(styles, /\.client-rule-outbound \{[\s\S]*width: 34px/);
|
||||||
assert.match(styles, /\.client-rule-outbound svg \{[\s\S]*width: 20px/);
|
assert.match(styles, /\.client-rule-outbound svg \{[\s\S]*width: 20px/);
|
||||||
assert.match(styles, /\.client-local-rules-list\.has-order-flow::before[\s\S]*linear-gradient\(to bottom/);
|
assert.match(styles, /\.client-local-rules-list\.has-order-flow::before[\s\S]*linear-gradient\(to bottom/);
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ test('center crossing moves one or many slots without changing stable row identi
|
|||||||
assert.equal(crossedRuleIndex(0, 149, [100, 150, 200]), 0);
|
assert.equal(crossedRuleIndex(0, 149, [100, 150, 200]), 0);
|
||||||
assert.equal(crossedRuleIndex(0, 151, [100, 150, 200]), 1);
|
assert.equal(crossedRuleIndex(0, 151, [100, 150, 200]), 1);
|
||||||
assert.equal(crossedRuleIndex(0, 201, [100, 150, 200]), 2);
|
assert.equal(crossedRuleIndex(0, 201, [100, 150, 200]), 2);
|
||||||
|
assert.equal(crossedRuleIndex(2, 149, [100, 150, 200]), 1);
|
||||||
|
assert.equal(crossedRuleIndex(2, 99, [100, 150, 200]), 0);
|
||||||
const moved = moveRule(rules, 0, 2);
|
const moved = moveRule(rules, 0, 2);
|
||||||
assert.deepEqual(moved.map(({ _key }) => _key), ['b', 'c', 'a']);
|
assert.deepEqual(moved.map(({ _key }) => _key), ['b', 'c', 'a']);
|
||||||
assert.strictEqual(moved[2], rules[0]);
|
assert.strictEqual(moved[2], rules[0]);
|
||||||
|
|||||||
@@ -37,26 +37,26 @@ const expectedImports = [
|
|||||||
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
|
||||||
const acceptedLedger = {
|
const acceptedLedger = {
|
||||||
counts: {
|
counts: {
|
||||||
cascadeEdges: 949,
|
cascadeEdges: 950,
|
||||||
customProperties: 108,
|
customProperties: 108,
|
||||||
declarations: 3503,
|
declarations: 3500,
|
||||||
important: 0,
|
important: 0,
|
||||||
keyframes: 49,
|
keyframes: 49,
|
||||||
media: 13,
|
media: 13,
|
||||||
rules: 984,
|
rules: 984,
|
||||||
variableReferences: 844,
|
variableReferences: 840,
|
||||||
},
|
},
|
||||||
hashes: {
|
hashes: {
|
||||||
cascadeEdges: '4c0a4c67d60c95181f4267225cbcba7e24eac3355be5e6707db9ed947a4fb54f',
|
cascadeEdges: '5d255c2f624bf3b45037c027f127d644f02dfc5a130313e766597da9bee4875f',
|
||||||
customProperties: '3b97add4c3d685a532afff5046ddbe2b6eebcc0866c62781348559897f2930fb',
|
customProperties: '3b97add4c3d685a532afff5046ddbe2b6eebcc0866c62781348559897f2930fb',
|
||||||
declarations: 'f630814428eeb4fef56795e2b8323c0fd567bc3d2334c5603973893d2a54c9f7',
|
declarations: '7b64892ae650b7035a2eadc2846f99c1085df66995637cdf0e0c807743275c38',
|
||||||
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
|
duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945',
|
||||||
duplicateSelectors: 'e87ccd4a057619f7ae0791564b50867d47473121bf0b0468cf1aceeafb806a1a',
|
duplicateSelectors: '8982145dba05b33bf1c93b2d54cfbe76305b276ff3c657aa020144016ada5848',
|
||||||
keyframes: '9e78309512ed82b1e9f87c58aeff505dfcdb694fc30c8f54570d01dce13eb51a',
|
keyframes: '9e78309512ed82b1e9f87c58aeff505dfcdb694fc30c8f54570d01dce13eb51a',
|
||||||
ruleDeclarationSequences: 'b6105ca3e89ca8a4b0f1c3b7e1ed7d1556da7afc856db19d3b36cc48e2de96b0',
|
ruleDeclarationSequences: '5e4f3127541777f8c404f2cc7c430bc42b3d0e15389f13cb62e7c5321d86897d',
|
||||||
selectors: 'd46d8c4b327f0c7ad2684ee05bf2338271c648fe0548315331c2a8719632e424',
|
selectors: '744f667e7e36762ab798ef84fcb41abba655fc087e6362dbc1e71b9f637ac259',
|
||||||
variableReferences: '74c781fe61c4ca9734dc76415d4335612e0a0d7c26b3b3731d1bc0bd3c870b5b',
|
variableReferences: '924d3c0fa5aa34a4afeeec4870e3a0f3e63c4144a930e90289da232fa96075ae',
|
||||||
witnesses: 'c1248557ba723246417861681ca79c407b3aa47aee1b5e45fb464fb9c135c711',
|
witnesses: 'aaaa1b6c3ac84e82646c8c8fd4d88f0c3c7cd291eed7d4c85ede32f2f9a2fea2',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -209,7 +209,7 @@ test('client typography uses the shared semantic scale outside the token owner',
|
|||||||
|
|
||||||
test('accepted stylesheet has pinned declaration, selector, keyframe, variable, and cascade ledgers', () => {
|
test('accepted stylesheet has pinned declaration, selector, keyframe, variable, and cascade ledgers', () => {
|
||||||
const witnesses = readStyleWitnesses(root);
|
const witnesses = readStyleWitnesses(root);
|
||||||
assert.equal(witnesses.length, 854);
|
assert.equal(witnesses.length, 859);
|
||||||
assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0);
|
assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0);
|
||||||
const ledger = createStyleLedger(readStyleSource(root), { witnesses });
|
const ledger = createStyleLedger(readStyleSource(root), { witnesses });
|
||||||
assert.deepEqual(ledger.counts, acceptedLedger.counts);
|
assert.deepEqual(ledger.counts, acceptedLedger.counts);
|
||||||
@@ -405,8 +405,8 @@ test('main owns one public stylesheet and the regrouped production CSS is determ
|
|||||||
assert.equal((main.match(/import ['"][^'"]+\.css['"]/g) || []).length, 1);
|
assert.equal((main.match(/import ['"][^'"]+\.css['"]/g) || []).length, 1);
|
||||||
|
|
||||||
const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css'));
|
const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css'));
|
||||||
assert.deepEqual(assets, ['index-Ch0T5A1e.css']);
|
assert.deepEqual(assets, ['index-CAi7iXH5.css']);
|
||||||
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
|
const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0]));
|
||||||
assert.equal(built.byteLength, 135594);
|
assert.equal(built.byteLength, 135653);
|
||||||
assert.equal(sha256(built), '8425efe616036b5b37f0acc7476c1ef01c0c5168bb5e531863e498ca20a8dfa8');
|
assert.equal(sha256(built), '15cb8f4597c16fd8ba119395b1246e3cf9362fc24988aecd6a08a0666fb53d46');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user