Skip to content

Commit 64bfbee

Browse files
committed
feat(core): drag handle
Signed-off-by: Cory Rylan <crylan@nvidia.com>
1 parent 9d99136 commit 64bfbee

17 files changed

Lines changed: 606 additions & 3 deletions
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading

‎projects/core/package.json‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,18 @@
323323
"types": "./dist/dot/define.d.ts",
324324
"default": "./dist/dot/define.js"
325325
},
326+
"./drag-handle": {
327+
"types": "./dist/drag-handle/index.d.ts",
328+
"default": "./dist/drag-handle/index.js"
329+
},
330+
"./drag-handle/index.js": {
331+
"types": "./dist/drag-handle/index.d.ts",
332+
"default": "./dist/drag-handle/index.js"
333+
},
334+
"./drag-handle/define.js": {
335+
"types": "./dist/drag-handle/define.d.ts",
336+
"default": "./dist/drag-handle/define.js"
337+
},
326338
"./drawer": {
327339
"types": "./dist/drawer/index.d.ts",
328340
"default": "./dist/drawer/index.js"

‎projects/core/src/bundle.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import '@nvidia-elements/core/datetime/define.js';
2222
import '@nvidia-elements/core/dialog/define.js';
2323
import '@nvidia-elements/core/divider/define.js';
2424
import '@nvidia-elements/core/dot/define.js';
25+
import '@nvidia-elements/core/drag-handle/define.js';
2526
import '@nvidia-elements/core/drawer/define.js';
2627
import '@nvidia-elements/core/dropdown/define.js';
2728
import '@nvidia-elements/core/dropdown-group/define.js';
@@ -94,6 +95,7 @@ export * from '@nvidia-elements/core/datetime';
9495
export * from '@nvidia-elements/core/dialog';
9596
export * from '@nvidia-elements/core/divider';
9697
export * from '@nvidia-elements/core/dot';
98+
export * from '@nvidia-elements/core/drag-handle';
9799
export * from '@nvidia-elements/core/drawer';
98100
export * from '@nvidia-elements/core/dropdown';
99101
export * from '@nvidia-elements/core/dropdown-group';
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { define } from '@nvidia-elements/core/internal';
5+
import { DragHandle } from '@nvidia-elements/core/drag-handle';
6+
7+
define(DragHandle);
8+
9+
declare global {
10+
interface HTMLElementTagNameMap {
11+
'nve-drag-handle': DragHandle;
12+
}
13+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. */
2+
/* SPDX-License-Identifier: Apache-2.0 */
3+
4+
:host {
5+
--nve-sys-interaction-state-base: transparent;
6+
--cursor: move;
7+
--color: var(--nve-sys-text-muted-color);
8+
--width: var(--nve-ref-size-800);
9+
--height: var(--nve-ref-size-800);
10+
--border-radius: var(--nve-ref-border-radius-xs);
11+
display: inline-flex;
12+
user-select: none;
13+
}
14+
15+
:host([draggable='true']) {
16+
-webkit-user-drag: element;
17+
}
18+
19+
:host(:state(disabled)),
20+
:host([readonly]) {
21+
-webkit-user-drag: none;
22+
}
23+
24+
[internal-host] {
25+
display: flex;
26+
align-items: center;
27+
justify-content: center;
28+
width: var(--width);
29+
height: var(--height);
30+
border-radius: var(--border-radius);
31+
cursor: var(--cursor);
32+
}
33+
34+
nve-icon {
35+
pointer-events: none;
36+
}
37+
38+
:host(:state(pressed)) {
39+
--color: var(--nve-sys-interaction-selected-color);
40+
}
41+
42+
:host(:hover:not(:state(disabled), [readonly], :state(pressed))) {
43+
--color: var(--nve-sys-interaction-hover-color);
44+
}
45+
46+
:host(:state(pressed)) [interaction-state] {
47+
--nve-sys-interaction-state-ratio: var(--nve-sys-interaction-state-ratio-selected);
48+
}
49+
50+
@container style(--nve-sys-color-scheme: dark) {
51+
:host {
52+
--nve-sys-interaction-state-ratio-selected: 16% !important;
53+
--nve-sys-interaction-state-ratio-hover: 12% !important;
54+
}
55+
}
56+
57+
:host(:state(disabled)) {
58+
--color: var(--nve-sys-interaction-disabled-color);
59+
--cursor: default;
60+
}
61+
62+
:host(:state(disabled)) [interaction-state] {
63+
--nve-sys-interaction-state-ratio: 0;
64+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { html } from 'lit';
5+
import '@nvidia-elements/core/drag-handle/define.js';
6+
7+
export default {
8+
title: 'Elements/Drag Handle',
9+
component: 'nve-drag-handle'
10+
};
11+
12+
/**
13+
* @summary Labeled grip for application-defined dragging and reordering. Use when an item needs a dedicated move control.
14+
*/
15+
export const Default = {
16+
render: () => html`<nve-drag-handle aria-label="move item"></nve-drag-handle>`
17+
};
18+
19+
/**
20+
* @summary Pressed grip for an item selected for movement. Use to show the handle state while the application manages reordering.
21+
*/
22+
export const Pressed = {
23+
render: () => html`<nve-drag-handle pressed aria-label="move item"></nve-drag-handle>`
24+
};
25+
26+
/**
27+
* @summary Disabled grip for an item that cannot move. Use to keep the control visible while movement is unavailable.
28+
*/
29+
export const Disabled = {
30+
render: () => html`<nve-drag-handle disabled aria-label="move item"></nve-drag-handle>`
31+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { html } from 'lit';
5+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6+
import { createFixture, elementIsStable, removeFixture } from '@internals/testing';
7+
import { runAxe } from '@internals/testing/axe';
8+
import { DragHandle } from '@nvidia-elements/core/drag-handle';
9+
import '@nvidia-elements/core/drag-handle/define.js';
10+
11+
describe(DragHandle.metadata.tag, () => {
12+
let fixture: HTMLElement;
13+
14+
beforeEach(async () => {
15+
fixture = await createFixture(html`
16+
<nve-drag-handle aria-label="Move item"></nve-drag-handle>
17+
<nve-drag-handle pressed aria-label="Move selected item"></nve-drag-handle>
18+
<nve-drag-handle disabled aria-label="Move unavailable item"></nve-drag-handle>
19+
<nve-drag-handle pressed disabled aria-label="Move unavailable selected item"></nve-drag-handle>
20+
<span id="move-item-label">Move labeled item</span>
21+
<nve-drag-handle aria-labelledby="move-item-label"></nve-drag-handle>
22+
`);
23+
await Promise.all(Array.from(fixture.querySelectorAll('nve-drag-handle'), element => elementIsStable(element)));
24+
});
25+
26+
afterEach(() => {
27+
removeFixture(fixture);
28+
});
29+
30+
it('should pass axe for default, pressed, disabled, and labeled states', async () => {
31+
const results = await runAxe([DragHandle.metadata.tag]);
32+
expect(results.violations).toHaveLength(0);
33+
});
34+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { describe, expect, test } from 'vitest';
5+
import { lighthouseRunner } from '@internals/vite';
6+
7+
describe('drag-handle lighthouse report', () => {
8+
test('should meet Lighthouse benchmarks', async () => {
9+
const report = await lighthouseRunner.getReport('nve-drag-handle', /* html */ `
10+
<nve-drag-handle aria-label="Move item"></nve-drag-handle>
11+
<script type="module">
12+
import '@nvidia-elements/core/drag-handle/define.js';
13+
</script>
14+
`);
15+
expect(report.scores.performance).toBe(100);
16+
expect(report.scores.accessibility).toBe(100);
17+
expect(report.scores.bestPractices).toBe(100);
18+
expect(report.payload.javascript.kb).toBeLessThan(19.3);
19+
});
20+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { html } from 'lit';
5+
import { describe, expect, it } from 'vitest';
6+
import { ssrRunner } from '@internals/vite';
7+
import { DragHandle } from '@nvidia-elements/core/drag-handle';
8+
import '@nvidia-elements/core/drag-handle/define.js';
9+
10+
describe(DragHandle.metadata.tag, () => {
11+
it.each(['default', 'pressed', 'disabled'])('should render the fixed grip in the %s state', async state => {
12+
const result = await ssrRunner.render(html`
13+
<nve-drag-handle ?pressed=${state === 'pressed'} ?disabled=${state === 'disabled'} aria-label="Move item"></nve-drag-handle>
14+
`);
15+
expect(result).toContain('shadowroot="open"');
16+
expect(result).toContain('nve-drag-handle');
17+
expect(result).toContain('name="drag"');
18+
expect(result).toContain('aria-hidden="true"');
19+
});
20+
});

0 commit comments

Comments
 (0)