@mfp-design-system/select 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/select.d.ts +39 -0
- package/dist/select.d.ts.map +1 -0
- package/dist/select.js +287 -0
- package/dist/select.js.map +1 -0
- package/package.json +50 -0
- package/src/index.ts +2 -0
- package/src/select.stories.ts +124 -0
- package/src/select.ts +286 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Melissa Pula
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @mfp-design-system/select
|
|
2
|
+
|
|
3
|
+
A Lit-based `<mfp-select>` web component that wraps a native `<select>` element.
|
|
4
|
+
|
|
5
|
+
## Why a native wrapper?
|
|
6
|
+
|
|
7
|
+
Native `<select>` gets you full keyboard a11y, screen reader support, and mobile-native pickers (iOS wheel, Android sheet) for free. Only the visual chrome (border, focus ring, chevron) is custom. A full custom-combobox implementation is on the roadmap if/when you need things like search-as-you-type or custom option rendering.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install @mfp-design-system/select @mfp-design-system/tokens
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import '@mfp-design-system/select';
|
|
19
|
+
import '@mfp-design-system/tokens/css';
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```html
|
|
23
|
+
<mfp-select label="Color" placeholder="Pick one…">
|
|
24
|
+
<option value="red">Red</option>
|
|
25
|
+
<option value="green">Green</option>
|
|
26
|
+
<option value="blue">Blue</option>
|
|
27
|
+
</mfp-select>
|
|
28
|
+
|
|
29
|
+
<mfp-select label="Fruit" placeholder="Choose…">
|
|
30
|
+
<optgroup label="Citrus">
|
|
31
|
+
<option value="orange">Orange</option>
|
|
32
|
+
<option value="lemon">Lemon</option>
|
|
33
|
+
</optgroup>
|
|
34
|
+
<optgroup label="Berries">
|
|
35
|
+
<option value="strawberry">Strawberry</option>
|
|
36
|
+
</optgroup>
|
|
37
|
+
</mfp-select>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Children projected into the default slot (`<option>` and `<optgroup>` elements) are forwarded into the real `<select>` inside shadow DOM. The slot itself is hidden.
|
|
41
|
+
|
|
42
|
+
## API
|
|
43
|
+
|
|
44
|
+
| Attribute | Type | Default | Description |
|
|
45
|
+
| ------------- | ---------------------- | ------- | ------------------------------------------------------------ |
|
|
46
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Sizing |
|
|
47
|
+
| `value` | `string` | `''` | Current value (controlled) |
|
|
48
|
+
| `name` | `string` | `''` | Native form `name` |
|
|
49
|
+
| `label` | `string` | `''` | Visible label (auto-wired with `for`/`id`) |
|
|
50
|
+
| `placeholder` | `string` | `''` | Renders a disabled, hidden first option as the initial value |
|
|
51
|
+
| `hint` | `string` | `''` | Helper text shown below the select |
|
|
52
|
+
| `error` | `string` | `''` | Error message — also triggers invalid styles |
|
|
53
|
+
| `disabled` | `boolean` | `false` | Disables the select |
|
|
54
|
+
| `required` | `boolean` | `false` | Marks as required (adds \* to label) |
|
|
55
|
+
|
|
56
|
+
### Events
|
|
57
|
+
|
|
58
|
+
- `change` — fires when the value changes. `event.detail.value` carries the new value.
|
|
59
|
+
|
|
60
|
+
### Shadow parts
|
|
61
|
+
|
|
62
|
+
For custom styling: `label`, `control`, `select`, `chevron`, `hint`, `error`.
|
|
63
|
+
|
|
64
|
+
```css
|
|
65
|
+
mfp-select::part(chevron) {
|
|
66
|
+
color: var(--color-status-info-solid);
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Framework notes
|
|
71
|
+
|
|
72
|
+
Same as the rest of the suite — Vue/Nuxt need `isCustomElement`, Angular needs `CUSTOM_ELEMENTS_SCHEMA`.
|
|
73
|
+
|
|
74
|
+
## Known limitations
|
|
75
|
+
|
|
76
|
+
- Single-select only. Multi-select via the native `<select multiple>` UI is uglier than most teams want — multi-select is on the roadmap as a separate component (`<mfp-multi-select>`) with a checkbox-list pattern.
|
|
77
|
+
- Form submission via native form: the inner `<select>` is in shadow DOM, so it does not participate in native form submission yet. Listen for `change` events; full `ElementInternals` form-association is on the roadmap for all form components.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/select.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { LitElement } from 'lit';
|
|
2
|
+
export type SelectSize = 'sm' | 'md' | 'lg';
|
|
3
|
+
/**
|
|
4
|
+
* `<mfp-select>` wraps a native `<select>` element. Children projected into
|
|
5
|
+
* the default slot are forwarded into the real select — usage:
|
|
6
|
+
*
|
|
7
|
+
* <mfp-select label="Color">
|
|
8
|
+
* <option value="red">Red</option>
|
|
9
|
+
* <option value="green">Green</option>
|
|
10
|
+
* </mfp-select>
|
|
11
|
+
*
|
|
12
|
+
* Using the native control means full keyboard a11y, screen reader support,
|
|
13
|
+
* and mobile-native pickers come for free. The visual chrome (border, focus
|
|
14
|
+
* ring, chevron) is styled via shadow CSS using design tokens.
|
|
15
|
+
*/
|
|
16
|
+
export declare class MfpSelect extends LitElement {
|
|
17
|
+
static styles: import("lit").CSSResult;
|
|
18
|
+
size: SelectSize;
|
|
19
|
+
value: string;
|
|
20
|
+
name: string;
|
|
21
|
+
label: string;
|
|
22
|
+
placeholder: string;
|
|
23
|
+
hint: string;
|
|
24
|
+
error: string;
|
|
25
|
+
disabled: boolean;
|
|
26
|
+
required: boolean;
|
|
27
|
+
private _id;
|
|
28
|
+
private _selectEl;
|
|
29
|
+
private _onChange;
|
|
30
|
+
/** Move slotted <option> elements into the real <select> when they change. */
|
|
31
|
+
private _onSlotChange;
|
|
32
|
+
render(): import("lit").TemplateResult<1>;
|
|
33
|
+
}
|
|
34
|
+
declare global {
|
|
35
|
+
interface HTMLElementTagNameMap {
|
|
36
|
+
'mfp-select': MfpSelect;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=select.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"select.d.ts","sourceRoot":"","sources":["../src/select.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAsB,MAAM,KAAK,CAAC;AAGrD,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAI5C;;;;;;;;;;;;GAYG;AACH,qBACa,SAAU,SAAQ,UAAU;IACrC,OAAgB,MAAM,0BA2HpB;IAGF,IAAI,EAAE,UAAU,CAAQ;IAGxB,KAAK,SAAM;IAGX,IAAI,SAAM;IAGV,KAAK,SAAM;IAGX,WAAW,SAAM;IAGjB,IAAI,SAAM;IAGV,KAAK,SAAM;IAGX,QAAQ,UAAS;IAGjB,QAAQ,UAAS;IAEjB,OAAO,CAAC,GAAG,CAAqC;IAGhD,OAAO,CAAC,SAAS,CAAqB;IAEtC,OAAO,CAAC,SAAS,CAUf;IAEF,8EAA8E;IAC9E,OAAO,CAAC,aAAa,CAyBnB;IAEO,MAAM;CA4DlB;AAED,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,YAAY,EAAE,SAAS,CAAC;KAC3B;CACJ"}
|
package/dist/select.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { LitElement, html, css, nothing } from 'lit';
|
|
8
|
+
import { customElement, property, query } from 'lit/decorators.js';
|
|
9
|
+
let selectIdCounter = 0;
|
|
10
|
+
/**
|
|
11
|
+
* `<mfp-select>` wraps a native `<select>` element. Children projected into
|
|
12
|
+
* the default slot are forwarded into the real select — usage:
|
|
13
|
+
*
|
|
14
|
+
* <mfp-select label="Color">
|
|
15
|
+
* <option value="red">Red</option>
|
|
16
|
+
* <option value="green">Green</option>
|
|
17
|
+
* </mfp-select>
|
|
18
|
+
*
|
|
19
|
+
* Using the native control means full keyboard a11y, screen reader support,
|
|
20
|
+
* and mobile-native pickers come for free. The visual chrome (border, focus
|
|
21
|
+
* ring, chevron) is styled via shadow CSS using design tokens.
|
|
22
|
+
*/
|
|
23
|
+
let MfpSelect = class MfpSelect extends LitElement {
|
|
24
|
+
constructor() {
|
|
25
|
+
super(...arguments);
|
|
26
|
+
this.size = 'md';
|
|
27
|
+
this.value = '';
|
|
28
|
+
this.name = '';
|
|
29
|
+
this.label = '';
|
|
30
|
+
this.placeholder = '';
|
|
31
|
+
this.hint = '';
|
|
32
|
+
this.error = '';
|
|
33
|
+
this.disabled = false;
|
|
34
|
+
this.required = false;
|
|
35
|
+
this._id = `mfp-select-${++selectIdCounter}`;
|
|
36
|
+
this._onChange = (e) => {
|
|
37
|
+
const select = e.target;
|
|
38
|
+
this.value = select.value;
|
|
39
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
40
|
+
bubbles: true,
|
|
41
|
+
composed: true,
|
|
42
|
+
detail: { value: select.value },
|
|
43
|
+
}));
|
|
44
|
+
};
|
|
45
|
+
/** Move slotted <option> elements into the real <select> when they change. */
|
|
46
|
+
this._onSlotChange = (e) => {
|
|
47
|
+
const slot = e.target;
|
|
48
|
+
const select = this._selectEl;
|
|
49
|
+
if (!select)
|
|
50
|
+
return;
|
|
51
|
+
const currentValue = this.value;
|
|
52
|
+
const optionLikeNodes = slot
|
|
53
|
+
.assignedNodes({ flatten: true })
|
|
54
|
+
.filter((n) => n.nodeType === Node.ELEMENT_NODE &&
|
|
55
|
+
(n.tagName === 'OPTION' ||
|
|
56
|
+
n.tagName === 'OPTGROUP'));
|
|
57
|
+
// Replace existing real options (other than the placeholder, which lives in the template)
|
|
58
|
+
const placeholder = select.querySelector('option[data-mfp-placeholder]');
|
|
59
|
+
select.textContent = '';
|
|
60
|
+
if (placeholder)
|
|
61
|
+
select.appendChild(placeholder);
|
|
62
|
+
for (const node of optionLikeNodes) {
|
|
63
|
+
select.appendChild(node.cloneNode(true));
|
|
64
|
+
}
|
|
65
|
+
// Restore the value (reflect attribute → DOM value)
|
|
66
|
+
select.value = currentValue;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
static { this.styles = css `
|
|
70
|
+
:host {
|
|
71
|
+
display: block;
|
|
72
|
+
font-family: var(--font-family-sans, system-ui, -apple-system, sans-serif);
|
|
73
|
+
color: var(--color-text-default, #111827);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
:host([disabled]) {
|
|
77
|
+
opacity: 0.6;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
label {
|
|
81
|
+
display: block;
|
|
82
|
+
font-size: var(--font-size-sm, 14px);
|
|
83
|
+
font-weight: var(--font-weight-medium, 500);
|
|
84
|
+
line-height: var(--font-line-height-tight, 1.2);
|
|
85
|
+
margin-bottom: var(--size-spacing-2, 8px);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.required {
|
|
89
|
+
color: var(--color-status-error-solid, #dc2626);
|
|
90
|
+
margin-left: var(--size-spacing-1, 4px);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.control {
|
|
94
|
+
position: relative;
|
|
95
|
+
display: flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
background: var(--color-neutral-0, #ffffff);
|
|
98
|
+
border: 1px solid var(--color-border-default, #e5e7eb);
|
|
99
|
+
border-radius: var(--size-radius-md, 8px);
|
|
100
|
+
transition:
|
|
101
|
+
border-color var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease),
|
|
102
|
+
box-shadow var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.control:focus-within {
|
|
106
|
+
border-color: var(--color-status-info-solid, #2563eb);
|
|
107
|
+
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.control.invalid {
|
|
111
|
+
border-color: var(--color-status-error-solid, #dc2626);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.control.invalid:focus-within {
|
|
115
|
+
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.15);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
select {
|
|
119
|
+
flex: 1 1 auto;
|
|
120
|
+
min-width: 0;
|
|
121
|
+
appearance: none;
|
|
122
|
+
-webkit-appearance: none;
|
|
123
|
+
background: transparent;
|
|
124
|
+
border: none;
|
|
125
|
+
outline: none;
|
|
126
|
+
font: inherit;
|
|
127
|
+
color: inherit;
|
|
128
|
+
cursor: pointer;
|
|
129
|
+
padding: var(--size-spacing-2, 8px) var(--size-spacing-9, 36px)
|
|
130
|
+
var(--size-spacing-2, 8px) var(--size-spacing-3, 12px);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
select:disabled {
|
|
134
|
+
cursor: not-allowed;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.chevron {
|
|
138
|
+
position: absolute;
|
|
139
|
+
right: var(--size-spacing-3, 12px);
|
|
140
|
+
top: 50%;
|
|
141
|
+
transform: translateY(-50%);
|
|
142
|
+
width: 1em;
|
|
143
|
+
height: 1em;
|
|
144
|
+
color: var(--color-text-muted, #6b7280);
|
|
145
|
+
pointer-events: none;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* Sizes — fall back to medium when no [size] attribute is set */
|
|
149
|
+
:host(:not([size])) select,
|
|
150
|
+
:host([size='md']) select {
|
|
151
|
+
font-size: var(--font-size-base, 16px);
|
|
152
|
+
min-height: 40px;
|
|
153
|
+
}
|
|
154
|
+
:host([size='sm']) select {
|
|
155
|
+
font-size: var(--font-size-sm, 14px);
|
|
156
|
+
min-height: 32px;
|
|
157
|
+
padding: var(--size-spacing-1, 4px) var(--size-spacing-9, 36px)
|
|
158
|
+
var(--size-spacing-1, 4px) var(--size-spacing-3, 12px);
|
|
159
|
+
}
|
|
160
|
+
:host([size='lg']) select {
|
|
161
|
+
font-size: var(--font-size-lg, 18px);
|
|
162
|
+
min-height: 48px;
|
|
163
|
+
padding: var(--size-spacing-3, 12px) var(--size-spacing-10, 40px)
|
|
164
|
+
var(--size-spacing-3, 12px) var(--size-spacing-4, 16px);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.hint,
|
|
168
|
+
.error {
|
|
169
|
+
margin: var(--size-spacing-2, 8px) 0 0;
|
|
170
|
+
font-size: var(--font-size-sm, 14px);
|
|
171
|
+
line-height: var(--font-line-height-tight, 1.2);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.hint {
|
|
175
|
+
color: var(--color-text-muted, #6b7280);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.error {
|
|
179
|
+
color: var(--color-status-error-solid, #dc2626);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* Hide the slot — its children get moved into the native select */
|
|
183
|
+
.options-source {
|
|
184
|
+
display: none;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@media (prefers-reduced-motion: reduce) {
|
|
188
|
+
.control {
|
|
189
|
+
transition: none;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
`; }
|
|
193
|
+
render() {
|
|
194
|
+
const invalid = this.error.length > 0;
|
|
195
|
+
const inputId = this._id;
|
|
196
|
+
const hintId = `${inputId}-hint`;
|
|
197
|
+
const errorId = `${inputId}-error`;
|
|
198
|
+
const describedBy = invalid ? errorId : this.hint ? hintId : undefined;
|
|
199
|
+
return html `
|
|
200
|
+
${this.label
|
|
201
|
+
? html `<label part="label" for=${inputId}>
|
|
202
|
+
${this.label}
|
|
203
|
+
${this.required
|
|
204
|
+
? html `<span class="required" aria-hidden="true">*</span>`
|
|
205
|
+
: nothing}
|
|
206
|
+
</label>`
|
|
207
|
+
: nothing}
|
|
208
|
+
<div part="control" class="control ${invalid ? 'invalid' : ''}">
|
|
209
|
+
<select
|
|
210
|
+
id=${inputId}
|
|
211
|
+
part="select"
|
|
212
|
+
.value=${this.value}
|
|
213
|
+
name=${this.name}
|
|
214
|
+
?disabled=${this.disabled}
|
|
215
|
+
?required=${this.required}
|
|
216
|
+
aria-invalid=${invalid ? 'true' : 'false'}
|
|
217
|
+
aria-describedby=${describedBy ?? nothing}
|
|
218
|
+
@change=${this._onChange}
|
|
219
|
+
>
|
|
220
|
+
${this.placeholder
|
|
221
|
+
? html `<option value="" disabled selected hidden data-mfp-placeholder>
|
|
222
|
+
${this.placeholder}
|
|
223
|
+
</option>`
|
|
224
|
+
: nothing}
|
|
225
|
+
</select>
|
|
226
|
+
<svg
|
|
227
|
+
class="chevron"
|
|
228
|
+
part="chevron"
|
|
229
|
+
viewBox="0 0 16 16"
|
|
230
|
+
fill="none"
|
|
231
|
+
aria-hidden="true"
|
|
232
|
+
>
|
|
233
|
+
<path
|
|
234
|
+
d="M4 6l4 4 4-4"
|
|
235
|
+
stroke="currentColor"
|
|
236
|
+
stroke-width="1.5"
|
|
237
|
+
stroke-linecap="round"
|
|
238
|
+
stroke-linejoin="round"
|
|
239
|
+
/>
|
|
240
|
+
</svg>
|
|
241
|
+
</div>
|
|
242
|
+
<div class="options-source">
|
|
243
|
+
<slot @slotchange=${this._onSlotChange}></slot>
|
|
244
|
+
</div>
|
|
245
|
+
${invalid
|
|
246
|
+
? html `<p part="error" id=${errorId} class="error" role="alert">${this.error}</p>`
|
|
247
|
+
: this.hint
|
|
248
|
+
? html `<p part="hint" id=${hintId} class="hint">${this.hint}</p>`
|
|
249
|
+
: nothing}
|
|
250
|
+
`;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
__decorate([
|
|
254
|
+
property({ reflect: true })
|
|
255
|
+
], MfpSelect.prototype, "size", void 0);
|
|
256
|
+
__decorate([
|
|
257
|
+
property()
|
|
258
|
+
], MfpSelect.prototype, "value", void 0);
|
|
259
|
+
__decorate([
|
|
260
|
+
property()
|
|
261
|
+
], MfpSelect.prototype, "name", void 0);
|
|
262
|
+
__decorate([
|
|
263
|
+
property()
|
|
264
|
+
], MfpSelect.prototype, "label", void 0);
|
|
265
|
+
__decorate([
|
|
266
|
+
property()
|
|
267
|
+
], MfpSelect.prototype, "placeholder", void 0);
|
|
268
|
+
__decorate([
|
|
269
|
+
property()
|
|
270
|
+
], MfpSelect.prototype, "hint", void 0);
|
|
271
|
+
__decorate([
|
|
272
|
+
property()
|
|
273
|
+
], MfpSelect.prototype, "error", void 0);
|
|
274
|
+
__decorate([
|
|
275
|
+
property({ type: Boolean, reflect: true })
|
|
276
|
+
], MfpSelect.prototype, "disabled", void 0);
|
|
277
|
+
__decorate([
|
|
278
|
+
property({ type: Boolean, reflect: true })
|
|
279
|
+
], MfpSelect.prototype, "required", void 0);
|
|
280
|
+
__decorate([
|
|
281
|
+
query('select')
|
|
282
|
+
], MfpSelect.prototype, "_selectEl", void 0);
|
|
283
|
+
MfpSelect = __decorate([
|
|
284
|
+
customElement('mfp-select')
|
|
285
|
+
], MfpSelect);
|
|
286
|
+
export { MfpSelect };
|
|
287
|
+
//# sourceMappingURL=select.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"select.js","sourceRoot":"","sources":["../src/select.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mfp-design-system/select",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Select dropdown web component for the mfp-design-system, built with Lit.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": true,
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"module": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"lit": "^3.2.1"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@mfp-design-system/tokens": "^0.2.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependenciesMeta": {
|
|
30
|
+
"@mfp-design-system/tokens": {
|
|
31
|
+
"optional": true
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"rimraf": "^6.0.1",
|
|
36
|
+
"typescript": "^5.6.3",
|
|
37
|
+
"@mfp-design-system/tokens": "0.2.0"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc -p tsconfig.build.json",
|
|
44
|
+
"dev": "tsc -p tsconfig.build.json --watch",
|
|
45
|
+
"clean": "rimraf dist",
|
|
46
|
+
"lint": "eslint . --max-warnings=0",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"test": "echo \"no tests yet\""
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/web-components';
|
|
2
|
+
import { html } from 'lit';
|
|
3
|
+
import './select.js';
|
|
4
|
+
import type { SelectSize } from './select.js';
|
|
5
|
+
|
|
6
|
+
interface Args {
|
|
7
|
+
label: string;
|
|
8
|
+
size: SelectSize;
|
|
9
|
+
placeholder: string;
|
|
10
|
+
value: string;
|
|
11
|
+
hint: string;
|
|
12
|
+
error: string;
|
|
13
|
+
disabled: boolean;
|
|
14
|
+
required: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const meta: Meta<Args> = {
|
|
18
|
+
title: 'Components/Select',
|
|
19
|
+
component: 'mfp-select',
|
|
20
|
+
tags: ['autodocs'],
|
|
21
|
+
argTypes: {
|
|
22
|
+
label: { control: 'text' },
|
|
23
|
+
size: { control: 'select', options: ['sm', 'md', 'lg'] },
|
|
24
|
+
placeholder: { control: 'text' },
|
|
25
|
+
value: { control: 'text' },
|
|
26
|
+
hint: { control: 'text' },
|
|
27
|
+
error: { control: 'text' },
|
|
28
|
+
disabled: { control: 'boolean' },
|
|
29
|
+
required: { control: 'boolean' },
|
|
30
|
+
},
|
|
31
|
+
args: {
|
|
32
|
+
label: 'Favorite color',
|
|
33
|
+
size: 'md',
|
|
34
|
+
placeholder: 'Pick one…',
|
|
35
|
+
value: '',
|
|
36
|
+
hint: '',
|
|
37
|
+
error: '',
|
|
38
|
+
disabled: false,
|
|
39
|
+
required: false,
|
|
40
|
+
},
|
|
41
|
+
render: (args) => html`
|
|
42
|
+
<mfp-select
|
|
43
|
+
label=${args.label}
|
|
44
|
+
size=${args.size}
|
|
45
|
+
placeholder=${args.placeholder}
|
|
46
|
+
.value=${args.value}
|
|
47
|
+
hint=${args.hint}
|
|
48
|
+
error=${args.error}
|
|
49
|
+
?disabled=${args.disabled}
|
|
50
|
+
?required=${args.required}
|
|
51
|
+
style="max-width: 320px;"
|
|
52
|
+
>
|
|
53
|
+
<option value="red">Red</option>
|
|
54
|
+
<option value="orange">Orange</option>
|
|
55
|
+
<option value="yellow">Yellow</option>
|
|
56
|
+
<option value="green">Green</option>
|
|
57
|
+
<option value="blue">Blue</option>
|
|
58
|
+
<option value="purple">Purple</option>
|
|
59
|
+
</mfp-select>
|
|
60
|
+
`,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export default meta;
|
|
64
|
+
|
|
65
|
+
type Story = StoryObj<Args>;
|
|
66
|
+
|
|
67
|
+
export const Default: Story = {};
|
|
68
|
+
|
|
69
|
+
export const WithHint: Story = {
|
|
70
|
+
args: { hint: 'Choose the color used for accents.' },
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const WithError: Story = {
|
|
74
|
+
args: { error: 'Please pick a color.' },
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const Preselected: Story = {
|
|
78
|
+
args: { value: 'green', placeholder: '' },
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const Required: Story = {
|
|
82
|
+
args: { required: true },
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const Sizes: Story = {
|
|
86
|
+
parameters: { controls: { disable: true } },
|
|
87
|
+
render: () => html`
|
|
88
|
+
<div style="display: flex; flex-direction: column; gap: 16px; max-width: 320px;">
|
|
89
|
+
<mfp-select size="sm" label="Small" placeholder="sm">
|
|
90
|
+
<option>One</option>
|
|
91
|
+
<option>Two</option>
|
|
92
|
+
</mfp-select>
|
|
93
|
+
<mfp-select size="md" label="Medium" placeholder="md">
|
|
94
|
+
<option>One</option>
|
|
95
|
+
<option>Two</option>
|
|
96
|
+
</mfp-select>
|
|
97
|
+
<mfp-select size="lg" label="Large" placeholder="lg">
|
|
98
|
+
<option>One</option>
|
|
99
|
+
<option>Two</option>
|
|
100
|
+
</mfp-select>
|
|
101
|
+
</div>
|
|
102
|
+
`,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const Disabled: Story = {
|
|
106
|
+
args: { disabled: true, value: 'red', placeholder: '' },
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const WithGroups: Story = {
|
|
110
|
+
parameters: { controls: { disable: true } },
|
|
111
|
+
render: () => html`
|
|
112
|
+
<mfp-select label="Pick a fruit" placeholder="Choose…" style="max-width: 320px;">
|
|
113
|
+
<optgroup label="Citrus">
|
|
114
|
+
<option value="orange">Orange</option>
|
|
115
|
+
<option value="lemon">Lemon</option>
|
|
116
|
+
<option value="lime">Lime</option>
|
|
117
|
+
</optgroup>
|
|
118
|
+
<optgroup label="Berries">
|
|
119
|
+
<option value="strawberry">Strawberry</option>
|
|
120
|
+
<option value="blueberry">Blueberry</option>
|
|
121
|
+
</optgroup>
|
|
122
|
+
</mfp-select>
|
|
123
|
+
`,
|
|
124
|
+
};
|
package/src/select.ts
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { LitElement, html, css, nothing } from 'lit';
|
|
2
|
+
import { customElement, property, query } from 'lit/decorators.js';
|
|
3
|
+
|
|
4
|
+
export type SelectSize = 'sm' | 'md' | 'lg';
|
|
5
|
+
|
|
6
|
+
let selectIdCounter = 0;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `<mfp-select>` wraps a native `<select>` element. Children projected into
|
|
10
|
+
* the default slot are forwarded into the real select — usage:
|
|
11
|
+
*
|
|
12
|
+
* <mfp-select label="Color">
|
|
13
|
+
* <option value="red">Red</option>
|
|
14
|
+
* <option value="green">Green</option>
|
|
15
|
+
* </mfp-select>
|
|
16
|
+
*
|
|
17
|
+
* Using the native control means full keyboard a11y, screen reader support,
|
|
18
|
+
* and mobile-native pickers come for free. The visual chrome (border, focus
|
|
19
|
+
* ring, chevron) is styled via shadow CSS using design tokens.
|
|
20
|
+
*/
|
|
21
|
+
@customElement('mfp-select')
|
|
22
|
+
export class MfpSelect extends LitElement {
|
|
23
|
+
static override styles = css`
|
|
24
|
+
:host {
|
|
25
|
+
display: block;
|
|
26
|
+
font-family: var(--font-family-sans, system-ui, -apple-system, sans-serif);
|
|
27
|
+
color: var(--color-text-default, #111827);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
:host([disabled]) {
|
|
31
|
+
opacity: 0.6;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
label {
|
|
35
|
+
display: block;
|
|
36
|
+
font-size: var(--font-size-sm, 14px);
|
|
37
|
+
font-weight: var(--font-weight-medium, 500);
|
|
38
|
+
line-height: var(--font-line-height-tight, 1.2);
|
|
39
|
+
margin-bottom: var(--size-spacing-2, 8px);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.required {
|
|
43
|
+
color: var(--color-status-error-solid, #dc2626);
|
|
44
|
+
margin-left: var(--size-spacing-1, 4px);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.control {
|
|
48
|
+
position: relative;
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
background: var(--color-neutral-0, #ffffff);
|
|
52
|
+
border: 1px solid var(--color-border-default, #e5e7eb);
|
|
53
|
+
border-radius: var(--size-radius-md, 8px);
|
|
54
|
+
transition:
|
|
55
|
+
border-color var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease),
|
|
56
|
+
box-shadow var(--motion-duration-fast, 150ms) var(--motion-easing-standard, ease);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.control:focus-within {
|
|
60
|
+
border-color: var(--color-status-info-solid, #2563eb);
|
|
61
|
+
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.control.invalid {
|
|
65
|
+
border-color: var(--color-status-error-solid, #dc2626);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.control.invalid:focus-within {
|
|
69
|
+
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.15);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
select {
|
|
73
|
+
flex: 1 1 auto;
|
|
74
|
+
min-width: 0;
|
|
75
|
+
appearance: none;
|
|
76
|
+
-webkit-appearance: none;
|
|
77
|
+
background: transparent;
|
|
78
|
+
border: none;
|
|
79
|
+
outline: none;
|
|
80
|
+
font: inherit;
|
|
81
|
+
color: inherit;
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
padding: var(--size-spacing-2, 8px) var(--size-spacing-9, 36px)
|
|
84
|
+
var(--size-spacing-2, 8px) var(--size-spacing-3, 12px);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
select:disabled {
|
|
88
|
+
cursor: not-allowed;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.chevron {
|
|
92
|
+
position: absolute;
|
|
93
|
+
right: var(--size-spacing-3, 12px);
|
|
94
|
+
top: 50%;
|
|
95
|
+
transform: translateY(-50%);
|
|
96
|
+
width: 1em;
|
|
97
|
+
height: 1em;
|
|
98
|
+
color: var(--color-text-muted, #6b7280);
|
|
99
|
+
pointer-events: none;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* Sizes — fall back to medium when no [size] attribute is set */
|
|
103
|
+
:host(:not([size])) select,
|
|
104
|
+
:host([size='md']) select {
|
|
105
|
+
font-size: var(--font-size-base, 16px);
|
|
106
|
+
min-height: 40px;
|
|
107
|
+
}
|
|
108
|
+
:host([size='sm']) select {
|
|
109
|
+
font-size: var(--font-size-sm, 14px);
|
|
110
|
+
min-height: 32px;
|
|
111
|
+
padding: var(--size-spacing-1, 4px) var(--size-spacing-9, 36px)
|
|
112
|
+
var(--size-spacing-1, 4px) var(--size-spacing-3, 12px);
|
|
113
|
+
}
|
|
114
|
+
:host([size='lg']) select {
|
|
115
|
+
font-size: var(--font-size-lg, 18px);
|
|
116
|
+
min-height: 48px;
|
|
117
|
+
padding: var(--size-spacing-3, 12px) var(--size-spacing-10, 40px)
|
|
118
|
+
var(--size-spacing-3, 12px) var(--size-spacing-4, 16px);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.hint,
|
|
122
|
+
.error {
|
|
123
|
+
margin: var(--size-spacing-2, 8px) 0 0;
|
|
124
|
+
font-size: var(--font-size-sm, 14px);
|
|
125
|
+
line-height: var(--font-line-height-tight, 1.2);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.hint {
|
|
129
|
+
color: var(--color-text-muted, #6b7280);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.error {
|
|
133
|
+
color: var(--color-status-error-solid, #dc2626);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* Hide the slot — its children get moved into the native select */
|
|
137
|
+
.options-source {
|
|
138
|
+
display: none;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@media (prefers-reduced-motion: reduce) {
|
|
142
|
+
.control {
|
|
143
|
+
transition: none;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
`;
|
|
147
|
+
|
|
148
|
+
@property({ reflect: true })
|
|
149
|
+
size: SelectSize = 'md';
|
|
150
|
+
|
|
151
|
+
@property()
|
|
152
|
+
value = '';
|
|
153
|
+
|
|
154
|
+
@property()
|
|
155
|
+
name = '';
|
|
156
|
+
|
|
157
|
+
@property()
|
|
158
|
+
label = '';
|
|
159
|
+
|
|
160
|
+
@property()
|
|
161
|
+
placeholder = '';
|
|
162
|
+
|
|
163
|
+
@property()
|
|
164
|
+
hint = '';
|
|
165
|
+
|
|
166
|
+
@property()
|
|
167
|
+
error = '';
|
|
168
|
+
|
|
169
|
+
@property({ type: Boolean, reflect: true })
|
|
170
|
+
disabled = false;
|
|
171
|
+
|
|
172
|
+
@property({ type: Boolean, reflect: true })
|
|
173
|
+
required = false;
|
|
174
|
+
|
|
175
|
+
private _id = `mfp-select-${++selectIdCounter}`;
|
|
176
|
+
|
|
177
|
+
@query('select')
|
|
178
|
+
private _selectEl!: HTMLSelectElement;
|
|
179
|
+
|
|
180
|
+
private _onChange = (e: Event) => {
|
|
181
|
+
const select = e.target as HTMLSelectElement;
|
|
182
|
+
this.value = select.value;
|
|
183
|
+
this.dispatchEvent(
|
|
184
|
+
new CustomEvent('change', {
|
|
185
|
+
bubbles: true,
|
|
186
|
+
composed: true,
|
|
187
|
+
detail: { value: select.value },
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/** Move slotted <option> elements into the real <select> when they change. */
|
|
193
|
+
private _onSlotChange = (e: Event) => {
|
|
194
|
+
const slot = e.target as HTMLSlotElement;
|
|
195
|
+
const select = this._selectEl;
|
|
196
|
+
if (!select) return;
|
|
197
|
+
|
|
198
|
+
const currentValue = this.value;
|
|
199
|
+
const optionLikeNodes = slot
|
|
200
|
+
.assignedNodes({ flatten: true })
|
|
201
|
+
.filter(
|
|
202
|
+
(n): n is HTMLElement =>
|
|
203
|
+
n.nodeType === Node.ELEMENT_NODE &&
|
|
204
|
+
((n as HTMLElement).tagName === 'OPTION' ||
|
|
205
|
+
(n as HTMLElement).tagName === 'OPTGROUP'),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Replace existing real options (other than the placeholder, which lives in the template)
|
|
209
|
+
const placeholder = select.querySelector('option[data-mfp-placeholder]');
|
|
210
|
+
select.textContent = '';
|
|
211
|
+
if (placeholder) select.appendChild(placeholder);
|
|
212
|
+
for (const node of optionLikeNodes) {
|
|
213
|
+
select.appendChild(node.cloneNode(true));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Restore the value (reflect attribute → DOM value)
|
|
217
|
+
select.value = currentValue;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
override render() {
|
|
221
|
+
const invalid = this.error.length > 0;
|
|
222
|
+
const inputId = this._id;
|
|
223
|
+
const hintId = `${inputId}-hint`;
|
|
224
|
+
const errorId = `${inputId}-error`;
|
|
225
|
+
const describedBy = invalid ? errorId : this.hint ? hintId : undefined;
|
|
226
|
+
|
|
227
|
+
return html`
|
|
228
|
+
${this.label
|
|
229
|
+
? html`<label part="label" for=${inputId}>
|
|
230
|
+
${this.label}
|
|
231
|
+
${this.required
|
|
232
|
+
? html`<span class="required" aria-hidden="true">*</span>`
|
|
233
|
+
: nothing}
|
|
234
|
+
</label>`
|
|
235
|
+
: nothing}
|
|
236
|
+
<div part="control" class="control ${invalid ? 'invalid' : ''}">
|
|
237
|
+
<select
|
|
238
|
+
id=${inputId}
|
|
239
|
+
part="select"
|
|
240
|
+
.value=${this.value}
|
|
241
|
+
name=${this.name}
|
|
242
|
+
?disabled=${this.disabled}
|
|
243
|
+
?required=${this.required}
|
|
244
|
+
aria-invalid=${invalid ? 'true' : 'false'}
|
|
245
|
+
aria-describedby=${describedBy ?? nothing}
|
|
246
|
+
@change=${this._onChange}
|
|
247
|
+
>
|
|
248
|
+
${this.placeholder
|
|
249
|
+
? html`<option value="" disabled selected hidden data-mfp-placeholder>
|
|
250
|
+
${this.placeholder}
|
|
251
|
+
</option>`
|
|
252
|
+
: nothing}
|
|
253
|
+
</select>
|
|
254
|
+
<svg
|
|
255
|
+
class="chevron"
|
|
256
|
+
part="chevron"
|
|
257
|
+
viewBox="0 0 16 16"
|
|
258
|
+
fill="none"
|
|
259
|
+
aria-hidden="true"
|
|
260
|
+
>
|
|
261
|
+
<path
|
|
262
|
+
d="M4 6l4 4 4-4"
|
|
263
|
+
stroke="currentColor"
|
|
264
|
+
stroke-width="1.5"
|
|
265
|
+
stroke-linecap="round"
|
|
266
|
+
stroke-linejoin="round"
|
|
267
|
+
/>
|
|
268
|
+
</svg>
|
|
269
|
+
</div>
|
|
270
|
+
<div class="options-source">
|
|
271
|
+
<slot @slotchange=${this._onSlotChange}></slot>
|
|
272
|
+
</div>
|
|
273
|
+
${invalid
|
|
274
|
+
? html`<p part="error" id=${errorId} class="error" role="alert">${this.error}</p>`
|
|
275
|
+
: this.hint
|
|
276
|
+
? html`<p part="hint" id=${hintId} class="hint">${this.hint}</p>`
|
|
277
|
+
: nothing}
|
|
278
|
+
`;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
declare global {
|
|
283
|
+
interface HTMLElementTagNameMap {
|
|
284
|
+
'mfp-select': MfpSelect;
|
|
285
|
+
}
|
|
286
|
+
}
|