Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit fc88b3a

Browse files
committed
feat(forms): add form control mixins
- button form control mixin - slider form control mixin - select form control mixin - checkbox form control mixin Signed-off-by: Cory Rylan <[email protected]>
1 parent 094563b commit fc88b3a

51 files changed

Lines changed: 6965 additions & 407 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

projects/forms/NOTICE.md

Lines changed: 1 addition & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,4 @@ NOTICE
33
Elements - NVIDIA Elements Design System
44
Copyright 2024-2026 NVIDIA Corporation
55

6-
This project includes the following bundled third-party software:
7-
8-
- lit v3.3.2 [BSD-3-Clause]
9-
Copyright: Google LLC
10-
11-
==============================================================================
12-
BSD-3-Clause
13-
==============================================================================
14-
15-
The following bundled components are provided under the BSD-3-Clause license:
16-
17-
lit v3.3.2 - Copyright Google LLC
18-
19-
Redistribution and use in source and binary forms, with or without
20-
modification, are permitted provided that the following conditions are met:
21-
22-
1. Redistributions of source code must retain the above copyright notice, this
23-
list of conditions and the following disclaimer.
24-
25-
2. Redistributions in binary form must reproduce the above copyright notice,
26-
this list of conditions and the following disclaimer in the documentation
27-
and/or other materials provided with the distribution.
28-
29-
3. Neither the name of the copyright holder nor the names of its
30-
contributors may be used to endorse or promote products derived from
31-
this software without specific prior written permission.
32-
33-
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
34-
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
35-
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
36-
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
37-
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
38-
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
39-
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
40-
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
41-
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
42-
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
43-
44-
For license details, see: BSD-3-Clause: https://opensource.org/licenses/BSD-3-Clause
6+
This product does not include any bundled third-party software.

projects/forms/eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default [
99
...litConfig,
1010
...jsonConfig,
1111
{
12-
files: ['src/mixin/**/*.ts'],
12+
files: ['src/mixins/**/*.ts'],
1313
rules: {
1414
// mixin factory functions
1515
'max-lines-per-function': 'off'

projects/forms/package.json

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,20 @@
5151
"default": "./dist/index.js"
5252
},
5353
"./mixin": {
54-
"types": "./dist/mixin/index.d.ts",
55-
"default": "./dist/mixin/index.js"
54+
"types": "./dist/mixins/index.d.ts",
55+
"default": "./dist/mixins/index.js"
5656
},
5757
"./mixin/index.js": {
58-
"types": "./dist/mixin/index.d.ts",
59-
"default": "./dist/mixin/index.js"
58+
"types": "./dist/mixins/index.d.ts",
59+
"default": "./dist/mixins/index.js"
60+
},
61+
"./mixins": {
62+
"types": "./dist/mixins/index.d.ts",
63+
"default": "./dist/mixins/index.js"
64+
},
65+
"./mixins/index.js": {
66+
"types": "./dist/mixins/index.d.ts",
67+
"default": "./dist/mixins/index.js"
6068
},
6169
"./validators": {
6270
"types": "./dist/validators/index.d.ts",
@@ -79,16 +87,14 @@
7987
"test:watch": "wireit",
8088
"test:lighthouse": "wireit"
8189
},
82-
"dependencies": {
83-
"lit": "catalog:publish"
84-
},
8590
"devDependencies": {
8691
"@eslint/js": "catalog:",
8792
"@nvidia-elements/styles": "workspace:*",
8893
"@internals/testing": "workspace:*",
8994
"@internals/eslint": "workspace:*",
9095
"@internals/vite": "workspace:*",
9196
"@nvidia-elements/lint": "workspace:*",
97+
"@typescript/lib-dom": "npm:@types/[email protected]",
9298
"@vitest/browser": "catalog:",
9399
"@vitest/coverage-istanbul": "catalog:",
94100
"axe-core": "catalog:",

projects/forms/src/examples/csv.examples.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import { css, html, LitElement, nothing } from 'lit';
55
import type { ValidatorResult } from '@nvidia-elements/forms';
6-
import { FormControlMixin } from '@nvidia-elements/forms/mixin';
6+
import { FormControlMixin } from '@nvidia-elements/forms/mixins';
77
import '@nvidia-elements/core/textarea/define.js';
88
import '@nvidia-elements/core/dropdown/define.js';
99
import '@nvidia-elements/core/checkbox/define.js';

projects/forms/src/examples/light.examples.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
import { html, LitElement, nothing, unsafeCSS } from 'lit';
5-
import { FormControlMixin } from '@nvidia-elements/forms/mixin';
5+
import { FormControlMixin } from '@nvidia-elements/forms/mixins';
66
import './light.examples.js';
77

88
export default {

projects/forms/src/examples/simple.examples.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
import { css, html, LitElement, nothing } from 'lit';
5-
import { FormControlMixin } from '@nvidia-elements/forms/mixin';
5+
import { FormControlMixin } from '@nvidia-elements/forms/mixins';
66

77
export default {
88
title: 'Labs/Forms/Examples'

projects/forms/src/examples/visualization.examples.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
import { css, html, LitElement, nothing } from 'lit';
5-
import { FormControlMixin } from '@nvidia-elements/forms/mixin';
5+
import { FormControlMixin } from '@nvidia-elements/forms/mixins';
66
import '@nvidia-elements/core/color/define.js';
77
import '@nvidia-elements/core/radio/define.js';
88
import '@nvidia-elements/core/range/define.js';
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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, removeFixture } from '@internals/testing';
7+
8+
import { StateActiveController } from './state-active.controller.js';
9+
import type { ReactiveController } from './types.js';
10+
11+
class StateActiveControllerTestElement extends HTMLElement {
12+
static formAssociated = true;
13+
14+
disabled = false;
15+
_internals?: ElementInternals;
16+
#controllers = new Set<ReactiveController>();
17+
18+
constructor() {
19+
super();
20+
new StateActiveController(this);
21+
}
22+
23+
addController(controller: ReactiveController) {
24+
this.#controllers.add(controller);
25+
}
26+
27+
connectedCallback() {
28+
this.#controllers.forEach(controller => controller.hostConnected?.());
29+
}
30+
31+
disconnectedCallback() {
32+
this.#controllers.forEach(controller => controller.hostDisconnected?.());
33+
}
34+
}
35+
36+
if (!customElements.get('state-active-controller-test-element')) {
37+
customElements.define('state-active-controller-test-element', StateActiveControllerTestElement);
38+
}
39+
40+
describe('StateActiveController', () => {
41+
let element: StateActiveControllerTestElement;
42+
let fixture: HTMLElement;
43+
44+
beforeEach(async () => {
45+
fixture = await createFixture(html`<state-active-controller-test-element></state-active-controller-test-element>`);
46+
element = fixture.querySelector<StateActiveControllerTestElement>('state-active-controller-test-element')!;
47+
});
48+
49+
afterEach(() => {
50+
removeFixture(fixture);
51+
});
52+
53+
it('should add and clear active state from pointer and keyboard events', () => {
54+
element.dispatchEvent(new MouseEvent('mousedown'));
55+
expect(element.matches(':state(active)')).toBe(true);
56+
57+
element.dispatchEvent(new MouseEvent('mouseup'));
58+
expect(element.matches(':state(active)')).toBe(false);
59+
60+
element.dispatchEvent(new KeyboardEvent('keypress', { code: 'Space' }));
61+
expect(element.matches(':state(active)')).toBe(true);
62+
63+
element.dispatchEvent(new KeyboardEvent('keyup'));
64+
expect(element.matches(':state(active)')).toBe(false);
65+
66+
element.dispatchEvent(new KeyboardEvent('keypress', { code: 'Enter' }));
67+
expect(element.matches(':state(active)')).toBe(true);
68+
69+
element.dispatchEvent(new FocusEvent('blur'));
70+
expect(element.matches(':state(active)')).toBe(false);
71+
});
72+
73+
it('should ignore disabled hosts and non-activation keys', () => {
74+
element.disabled = true;
75+
element.dispatchEvent(new MouseEvent('mousedown'));
76+
expect(element.matches(':state(active)')).toBe(false);
77+
78+
element.disabled = false;
79+
element.dispatchEvent(new KeyboardEvent('keypress', { code: 'KeyA' }));
80+
expect(element.matches(':state(active)')).toBe(false);
81+
});
82+
83+
it('should prevent default space keypress scrolling on the host', () => {
84+
const space = new KeyboardEvent('keypress', { code: 'Space', cancelable: true });
85+
const enter = new KeyboardEvent('keypress', { code: 'Enter', cancelable: true });
86+
87+
element.dispatchEvent(space);
88+
element.dispatchEvent(enter);
89+
90+
expect(space.defaultPrevented).toBe(true);
91+
expect(enter.defaultPrevented).toBe(false);
92+
});
93+
94+
it('should remove active listeners on disconnect', () => {
95+
element.remove();
96+
element.dispatchEvent(new MouseEvent('mousedown'));
97+
98+
expect(element.matches(':state(active)')).toBe(false);
99+
});
100+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { attachInternals } from '../utils.js';
5+
import type { ReactiveController, ReactiveElement } from './types.js';
6+
7+
type Active = ReactiveElement & { disabled: boolean; _internals?: ElementInternals };
8+
9+
export class StateActiveController<T extends Active> implements ReactiveController {
10+
constructor(private host: T) {
11+
this.host.addController(this);
12+
}
13+
14+
hostConnected() {
15+
attachInternals(this.host);
16+
this.host.addEventListener('keypress', this.#onActive as EventListener);
17+
this.host.addEventListener('mousedown', this.#onActive as EventListener);
18+
this.host.addEventListener('keyup', this.#onInactive);
19+
this.host.addEventListener('blur', this.#onInactive);
20+
this.host.addEventListener('mouseup', this.#onInactive);
21+
}
22+
23+
hostDisconnected() {
24+
this.host.removeEventListener('keypress', this.#onActive as EventListener);
25+
this.host.removeEventListener('mousedown', this.#onActive as EventListener);
26+
this.host.removeEventListener('keyup', this.#onInactive);
27+
this.host.removeEventListener('blur', this.#onInactive);
28+
this.host.removeEventListener('mouseup', this.#onInactive);
29+
}
30+
31+
#onActive = (event: KeyboardEvent | PointerEvent) => {
32+
if (!this.host.disabled && this.#isValidActiveEvent(event)) {
33+
this.host._internals!.states.add('active');
34+
}
35+
36+
if (event instanceof KeyboardEvent && event.code === 'Space' && event.target === this.host) {
37+
event.preventDefault();
38+
}
39+
};
40+
41+
#onInactive = () => {
42+
this.host._internals!.states.delete('active');
43+
};
44+
45+
#isValidActiveEvent(event: KeyboardEvent | PointerEvent) {
46+
return event instanceof KeyboardEvent ? event.code === 'Space' || event.code === 'Enter' : true;
47+
}
48+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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, removeFixture } from '@internals/testing';
7+
8+
import { StateCurrentController } from './state-current.controller.js';
9+
import type { ReactiveController } from './types.js';
10+
11+
class StateCurrentControllerTestElement extends HTMLElement {
12+
static formAssociated = true;
13+
14+
current?: string | null;
15+
readOnly = false;
16+
selected?: boolean | null;
17+
_internals?: ElementInternals;
18+
#controllers = new Set<ReactiveController>();
19+
20+
constructor() {
21+
super();
22+
new StateCurrentController(this);
23+
}
24+
25+
addController(controller: ReactiveController) {
26+
this.#controllers.add(controller);
27+
}
28+
29+
connectedCallback() {
30+
this.#controllers.forEach(controller => controller.hostConnected?.());
31+
}
32+
33+
sync() {
34+
this.#controllers.forEach(controller => controller.hostUpdated?.());
35+
}
36+
}
37+
38+
if (!customElements.get('state-current-controller-test-element')) {
39+
customElements.define('state-current-controller-test-element', StateCurrentControllerTestElement);
40+
}
41+
42+
describe('StateCurrentController', () => {
43+
let element: StateCurrentControllerTestElement;
44+
let fixture: HTMLElement;
45+
46+
beforeEach(async () => {
47+
fixture = await createFixture(
48+
html`<state-current-controller-test-element></state-current-controller-test-element>`
49+
);
50+
element = fixture.querySelector<StateCurrentControllerTestElement>('state-current-controller-test-element')!;
51+
});
52+
53+
afterEach(() => {
54+
removeFixture(fixture);
55+
});
56+
57+
it('should sync current aria and custom state', () => {
58+
element.current = 'page';
59+
element.sync();
60+
61+
expect(element._internals!.ariaCurrent).toBe('page');
62+
expect(element.matches(':state(current)')).toBe(true);
63+
64+
element.current = null;
65+
element.sync();
66+
67+
expect(element.matches(':state(current)')).toBe(false);
68+
});
69+
70+
it('should move current state to anchor aria-current for anchor hosts', () => {
71+
const anchor = document.createElement('a');
72+
element.append(anchor);
73+
element._internals!.states.add('anchor');
74+
75+
element.current = 'page';
76+
element.sync();
77+
78+
expect(element._internals!.ariaCurrent).toBe(null);
79+
expect(element.matches(':state(current)')).toBe(true);
80+
expect(anchor.getAttribute('aria-current')).toBe('page');
81+
82+
element.current = null;
83+
element.sync();
84+
85+
expect(anchor.hasAttribute('aria-current')).toBe(false);
86+
expect(element.matches(':state(current)')).toBe(false);
87+
});
88+
89+
it('should remove anchor aria-current while readonly', () => {
90+
const anchor = document.createElement('a');
91+
element.append(anchor);
92+
element._internals!.states.add('anchor');
93+
element.current = 'page';
94+
element.sync();
95+
96+
element.readOnly = true;
97+
element.sync();
98+
99+
expect(anchor.hasAttribute('aria-current')).toBe(false);
100+
});
101+
});

0 commit comments

Comments
 (0)