@jirawatpyk/aura-react 5.2.0 → 5.3.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.
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import * as React from "react";
3
- import { useStrings } from "./locale.js";
3
+ import { useStrings, useDensity } from "./locale.js";
4
4
  import { createPortal } from "react-dom";
5
5
  import { cx, uid, useMaybeControlled, useMounted, useIsoLayoutEffect, useMergedRef } from "./internal.js";
6
6
  import { Icon } from "./Icon.js";
@@ -24,6 +24,7 @@ const Combobox = React.forwardRef(
24
24
  const multi = all.multiple === true;
25
25
  const mp = all;
26
26
  const t = useStrings();
27
+ const density = useDensity();
27
28
  const auto = uid(), id = props.id || auto, listId = id + "-list";
28
29
  const options = (props.options || []).map(toOpt);
29
30
  const st = useMaybeControlled(
@@ -187,7 +188,7 @@ const Combobox = React.forwardRef(
187
188
  return id + "-opt-" + i;
188
189
  };
189
190
  const list = open && mounted && posState[0] ? createPortal(
190
- /* @__PURE__ */ React.createElement("div", { ref: listRef, className: "aura-combo__popover", style: posState[0] }, live.length ? /* @__PURE__ */ React.createElement(
191
+ /* @__PURE__ */ React.createElement("div", { ref: listRef, "data-density": density, className: "aura-combo__popover", style: posState[0] }, live.length ? /* @__PURE__ */ React.createElement(
191
192
  "ul",
192
193
  {
193
194
  id: listId,
@@ -1,9 +1,55 @@
1
1
  'use client';
2
2
  import * as React from "react";
3
+ import { createPortal } from "react-dom";
3
4
  import { FIELD_KEYS, Field, describedBy } from "./Field.js";
4
5
  import { Icon } from "./Icon.js";
5
- import { cx, omit, uid } from "./internal.js";
6
+ import { useDensity } from "./locale.js";
7
+ import { cx, omit, uid, useIsoLayoutEffect, useMergedRef, useMounted } from "./internal.js";
6
8
  const h = React.createElement;
9
+ const FORM_KEYS = ["name", "form", "value", "defaultValue", "disabled", "autoComplete", "onInput", "onInvalid"];
10
+ const HANDLED_KEYS = FORM_KEYS.concat([
11
+ "onChange",
12
+ "onBlur",
13
+ "onFocus",
14
+ "onKeyDown",
15
+ "onClick",
16
+ "autoFocus",
17
+ "tabIndex",
18
+ "aria-label",
19
+ "aria-labelledby",
20
+ "aria-describedby",
21
+ "multiple",
22
+ "size"
23
+ ]);
24
+ function pick(o, keys) {
25
+ const r = {};
26
+ for (const k of keys) if (k in o) r[k] = o[k];
27
+ return r;
28
+ }
29
+ function readItems(el) {
30
+ if (!el) return [];
31
+ return Array.prototype.map.call(el.options, function(o, i) {
32
+ const g = o.parentElement && o.parentElement.tagName === "OPTGROUP" ? o.parentElement : null;
33
+ return {
34
+ value: o.value,
35
+ label: o.textContent || "",
36
+ disabled: o.disabled || !!(g && g.disabled),
37
+ group: g ? g.label : null,
38
+ index: i
39
+ };
40
+ });
41
+ }
42
+ function initialLabel(props) {
43
+ const opts = (props.options || []).map(function(o) {
44
+ return typeof o === "object" ? o : { value: o, label: o };
45
+ });
46
+ const v = props.value !== void 0 && props.value !== null ? String(props.value) : props.defaultValue !== void 0 && props.defaultValue !== null ? String(props.defaultValue) : props.placeholder ? "" : opts[0] ? opts[0].value : "";
47
+ const hit = opts.filter(function(o) {
48
+ return o.value === v;
49
+ })[0];
50
+ if (hit) return { label: hit.label, empty: false };
51
+ return { label: props.placeholder || "", empty: true };
52
+ }
7
53
  const Select = React.forwardRef(function Select2(props, ref) {
8
54
  const auto = uid(), id = props.id || auto;
9
55
  const rest = omit(props, FIELD_KEYS.concat(["children"]));
@@ -16,10 +62,259 @@ const Select = React.forwardRef(function Select2(props, ref) {
16
62
  /* @__PURE__ */ React.createElement("option", { key: "__ph", value: "", disabled: true }, props.placeholder)
17
63
  );
18
64
  const extra = props.value === void 0 && props.defaultValue === void 0 && props.placeholder ? { defaultValue: "" } : {};
19
- return /* @__PURE__ */ React.createElement(
65
+ const native = !!props.multiple || props.size != null && props.size > 1;
66
+ const mounted = useMounted();
67
+ const live = !native && mounted;
68
+ const density = useDensity();
69
+ const selRef = React.useRef(null), trigRef = React.useRef(null), listRef = React.useRef(null);
70
+ const shown = React.useState(function() {
71
+ return initialLabel(props);
72
+ });
73
+ const openState = React.useState(false), open = openState[0];
74
+ const activeState = React.useState(-1), active = activeState[0];
75
+ const items = React.useState([]);
76
+ const pos = React.useState(null);
77
+ const typed = React.useRef({ text: "", at: 0 });
78
+ const sync = React.useCallback(function() {
79
+ const el = selRef.current;
80
+ if (!el) return;
81
+ const o = el.selectedIndex >= 0 ? el.options[el.selectedIndex] : null;
82
+ const next = o ? { label: o.textContent || "", empty: o.value === "" } : { label: "", empty: true };
83
+ shown[1](function(cur) {
84
+ return cur.label === next.label && cur.empty === next.empty ? cur : next;
85
+ });
86
+ }, []);
87
+ const hook = React.useCallback(
88
+ function(el) {
89
+ selRef.current = el;
90
+ if (!el || native || el.__auraHooked) return;
91
+ el.__auraHooked = true;
92
+ const proto = HTMLSelectElement.prototype;
93
+ const pv = Object.getOwnPropertyDescriptor(proto, "value"), pi = Object.getOwnPropertyDescriptor(proto, "selectedIndex");
94
+ if (pv && pv.set && pv.get)
95
+ Object.defineProperty(el, "value", {
96
+ configurable: true,
97
+ get: function() {
98
+ return pv.get.call(el);
99
+ },
100
+ set: function(v) {
101
+ pv.set.call(el, v);
102
+ sync();
103
+ }
104
+ });
105
+ if (pi && pi.set && pi.get)
106
+ Object.defineProperty(el, "selectedIndex", {
107
+ configurable: true,
108
+ get: function() {
109
+ return pi.get.call(el);
110
+ },
111
+ set: function(v) {
112
+ pi.set.call(el, v);
113
+ sync();
114
+ }
115
+ });
116
+ el.focus = function(o) {
117
+ if (trigRef.current) trigRef.current.focus(o);
118
+ else proto.focus.call(el, o);
119
+ };
120
+ },
121
+ [native, sync]
122
+ );
123
+ const selMerged = useMergedRef(ref, hook);
124
+ React.useEffect(
125
+ function() {
126
+ const f = selRef.current && selRef.current.form;
127
+ if (!f || native) return;
128
+ function onReset() {
129
+ setTimeout(sync, 0);
130
+ }
131
+ f.addEventListener("reset", onReset);
132
+ return function() {
133
+ f.removeEventListener("reset", onReset);
134
+ };
135
+ },
136
+ [native, sync]
137
+ );
138
+ const focusedOnce = React.useRef(false);
139
+ React.useEffect(
140
+ function() {
141
+ if (!live || focusedOnce.current || !trigRef.current) return;
142
+ focusedOnce.current = true;
143
+ if (props.autoFocus || selRef.current && document.activeElement === selRef.current) trigRef.current.focus();
144
+ },
145
+ [live]
146
+ );
147
+ useIsoLayoutEffect(function() {
148
+ if (!live) return;
149
+ sync();
150
+ if (!open) return;
151
+ const now = readItems(selRef.current), was = items[0];
152
+ const same = now.length === was.length && now.every(function(it, i) {
153
+ const w = was[i];
154
+ return w.value === it.value && w.label === it.label && w.disabled === it.disabled && w.group === it.group;
155
+ });
156
+ if (same) return;
157
+ items[1](now);
158
+ if (!choices(now).length) {
159
+ close(false);
160
+ return;
161
+ }
162
+ const hv = was[active] ? was[active].value : null;
163
+ const again = now.filter(function(it) {
164
+ return !it.disabled && it.value === hv;
165
+ })[0];
166
+ const cur = selRef.current ? selRef.current.selectedIndex : -1;
167
+ activeState[1](again ? again.index : now[cur] && !now[cur].disabled ? cur : choices(now)[0].index);
168
+ });
169
+ function choices(list) {
170
+ return list.filter(function(i) {
171
+ return !i.disabled;
172
+ });
173
+ }
174
+ function choose(it) {
175
+ const el = selRef.current;
176
+ if (!el || it.disabled) return;
177
+ if (el.value !== it.value) {
178
+ el.value = it.value;
179
+ el.dispatchEvent(new Event("input", { bubbles: true }));
180
+ el.dispatchEvent(new Event("change", { bubbles: true }));
181
+ }
182
+ sync();
183
+ close(true);
184
+ }
185
+ function openList(at) {
186
+ if (props.disabled || trigRef.current && trigRef.current.disabled) return;
187
+ const list = readItems(selRef.current);
188
+ const ok = choices(list);
189
+ if (!ok.length) return;
190
+ items[1](list);
191
+ const cur = selRef.current ? selRef.current.selectedIndex : -1;
192
+ let a = at === "first" ? ok[0].index : at === "last" ? ok[ok.length - 1].index : cur;
193
+ if (typeof at === "number") a = at;
194
+ if (a < 0 || !list[a] || list[a].disabled) a = ok[0].index;
195
+ activeState[1](a);
196
+ openState[1](true);
197
+ }
198
+ function close(focus) {
199
+ openState[1](false);
200
+ pos[1](null);
201
+ if (focus && trigRef.current) trigRef.current.focus();
202
+ }
203
+ function step(from, dir, list) {
204
+ for (let i = from + dir; i >= 0 && i < list.length; i += dir) if (!list[i].disabled) return i;
205
+ return from;
206
+ }
207
+ function typing() {
208
+ return !!typed.current.text && Date.now() - typed.current.at < 500;
209
+ }
210
+ function typeahead(ch, list, from) {
211
+ const now = Date.now(), t = typed.current;
212
+ t.text = now - t.at < 500 ? t.text + ch : ch;
213
+ t.at = now;
214
+ const q = t.text.toLocaleLowerCase();
215
+ for (let k = 1; k <= list.length; k++) {
216
+ const i = (from + (t.text.length > 1 ? 0 : 1) + k - 1 + list.length) % list.length;
217
+ if (!list[i].disabled && list[i].label.toLocaleLowerCase().indexOf(q) === 0) return i;
218
+ }
219
+ t.text = "";
220
+ return -1;
221
+ }
222
+ function onKeyDown(e) {
223
+ const k = e.key;
224
+ const printable = k.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey;
225
+ const search = printable && (k !== " " || typing());
226
+ if (!open) {
227
+ if (search) {
228
+ const list2 = readItems(selRef.current);
229
+ const i = typeahead(k, list2, selRef.current ? selRef.current.selectedIndex : -1);
230
+ e.preventDefault();
231
+ if (i >= 0) openList(i);
232
+ } else if (k === "ArrowDown" || k === "ArrowUp" || k === "Enter" || k === " ") {
233
+ e.preventDefault();
234
+ openList(k === "ArrowUp" && !(selRef.current && selRef.current.value) ? "last" : void 0);
235
+ }
236
+ return;
237
+ }
238
+ const list = items[0];
239
+ if (search) {
240
+ e.preventDefault();
241
+ const i = typeahead(k, list, active);
242
+ if (i >= 0) activeState[1](i);
243
+ } else if (k === "ArrowDown") {
244
+ e.preventDefault();
245
+ activeState[1](step(active, 1, list));
246
+ } else if (k === "ArrowUp") {
247
+ e.preventDefault();
248
+ activeState[1](step(active, -1, list));
249
+ } else if (k === "Home" || k === "PageUp") {
250
+ e.preventDefault();
251
+ activeState[1](k === "Home" ? step(-1, 1, list) : Math.max(step(-1, 1, list), step(active - 9, 1, list)));
252
+ } else if (k === "End" || k === "PageDown") {
253
+ e.preventDefault();
254
+ activeState[1](
255
+ k === "End" ? step(list.length, -1, list) : Math.min(step(list.length, -1, list), step(active + 9, -1, list))
256
+ );
257
+ } else if (k === "Enter" || k === " ") {
258
+ e.preventDefault();
259
+ if (list[active]) choose(list[active]);
260
+ } else if (k === "Escape") {
261
+ e.preventDefault();
262
+ e.stopPropagation();
263
+ close(true);
264
+ } else if (k === "Tab") {
265
+ close(false);
266
+ }
267
+ }
268
+ useIsoLayoutEffect(
269
+ function() {
270
+ if (!open) return;
271
+ function place() {
272
+ const t = trigRef.current && trigRef.current.parentElement;
273
+ if (!t) return;
274
+ const r = t.getBoundingClientRect(), below = window.innerHeight - r.bottom - 8, above = r.top - 8, want = listRef.current ? listRef.current.scrollHeight : 320, up = below < Math.min(want, 200) && above > below;
275
+ pos[1]({
276
+ left: r.left,
277
+ width: r.width,
278
+ top: up ? void 0 : r.bottom + 4,
279
+ bottom: up ? window.innerHeight - r.top + 4 : void 0,
280
+ maxHeight: Math.min(320, (up ? above : below) - 4)
281
+ });
282
+ }
283
+ place();
284
+ function outside(e) {
285
+ const n = e.target;
286
+ if (listRef.current && listRef.current.contains(n)) return;
287
+ if (trigRef.current && trigRef.current.contains(n)) return;
288
+ close(false);
289
+ }
290
+ function onScroll(e) {
291
+ if (!listRef.current || !listRef.current.contains(e.target)) place();
292
+ }
293
+ document.addEventListener("pointerdown", outside, true);
294
+ window.addEventListener("scroll", onScroll, true);
295
+ window.addEventListener("resize", place);
296
+ return function() {
297
+ document.removeEventListener("pointerdown", outside, true);
298
+ window.removeEventListener("scroll", onScroll, true);
299
+ window.removeEventListener("resize", place);
300
+ };
301
+ },
302
+ [open]
303
+ );
304
+ React.useEffect(
305
+ function() {
306
+ if (!open || !listRef.current || active < 0) return;
307
+ const el = listRef.current.querySelector('[data-idx="' + active + '"]');
308
+ if (el && el.scrollIntoView) el.scrollIntoView({ block: "nearest" });
309
+ },
310
+ [open, active, pos[0] != null]
311
+ );
312
+ const described = [describedBy(id, props), rest["aria-describedby"]].filter(Boolean).join(" ") || void 0;
313
+ const field = (child) => /* @__PURE__ */ React.createElement(
20
314
  Field,
21
315
  {
22
316
  id,
317
+ labelId: native ? void 0 : id + "-label",
23
318
  label: props.label,
24
319
  hint: props.hint,
25
320
  error: props.error,
@@ -28,19 +323,180 @@ const Select = React.forwardRef(function Select2(props, ref) {
28
323
  disabled: props.disabled,
29
324
  className: props.className
30
325
  },
31
- /* @__PURE__ */ React.createElement("div", { className: cx("aura-input aura-select", props.icon && "has-icon") }, props.icon ? /* @__PURE__ */ React.createElement(Icon, { name: props.icon, className: "aura-input__icon" }) : null, h(
32
- "select",
33
- Object.assign(extra, rest, {
34
- ref,
35
- id,
36
- className: "aura-input__control",
37
- required: props.required,
38
- "aria-invalid": props.error ? true : void 0,
39
- "aria-describedby": describedBy(id, props)
40
- }),
41
- opts,
42
- props.children
43
- ), /* @__PURE__ */ React.createElement(Icon, { name: "chevron-down", className: "aura-select__chevron" }))
326
+ child
327
+ );
328
+ if (!live)
329
+ return field(
330
+ /* @__PURE__ */ React.createElement("div", { className: cx("aura-input aura-select", props.icon && "has-icon") }, props.icon ? /* @__PURE__ */ React.createElement(Icon, { name: props.icon, className: "aura-input__icon" }) : null, h(
331
+ "select",
332
+ Object.assign(extra, rest, {
333
+ ref: native ? ref : selMerged,
334
+ id,
335
+ className: "aura-input__control",
336
+ required: props.required,
337
+ "aria-invalid": props.error ? true : void 0,
338
+ "aria-describedby": described
339
+ }),
340
+ opts,
341
+ props.children
342
+ ), /* @__PURE__ */ React.createElement(Icon, { name: "chevron-down", className: "aura-select__chevron" }))
343
+ );
344
+ const listId = id + "-list", labelId = id + "-label";
345
+ const optId = function(i) {
346
+ return id + "-opt-" + i;
347
+ };
348
+ const labelledBy = rest["aria-labelledby"] || (props.label ? labelId : void 0);
349
+ const selIndex = selRef.current ? selRef.current.selectedIndex : -1;
350
+ const option = function(it) {
351
+ const sel = selIndex === it.index;
352
+ return /* @__PURE__ */ React.createElement(
353
+ "div",
354
+ {
355
+ key: it.index,
356
+ id: optId(it.index),
357
+ role: "option",
358
+ "data-idx": it.index,
359
+ "aria-selected": sel,
360
+ "aria-disabled": it.disabled || void 0,
361
+ className: cx(
362
+ "aura-combo__option",
363
+ it.index === active && "is-active",
364
+ sel && "is-selected",
365
+ it.disabled && "is-disabled"
366
+ ),
367
+ onPointerDown: function(e) {
368
+ e.preventDefault();
369
+ },
370
+ onPointerMove: function() {
371
+ if (!it.disabled && active !== it.index) activeState[1](it.index);
372
+ },
373
+ onClick: function() {
374
+ choose(it);
375
+ }
376
+ },
377
+ /* @__PURE__ */ React.createElement("span", { className: "aura-combo__text" }, /* @__PURE__ */ React.createElement("span", { className: "aura-combo__label" }, it.label)),
378
+ sel ? /* @__PURE__ */ React.createElement(Icon, { name: "check", className: "aura-combo__check" }) : null
379
+ );
380
+ };
381
+ const runs = [];
382
+ items[0].forEach(function(it) {
383
+ if (it.value === "" && it.disabled) return;
384
+ const last = runs[runs.length - 1];
385
+ if (last && last.group === it.group) last.items.push(it);
386
+ else runs.push({ group: it.group, items: [it] });
387
+ });
388
+ const popup = open ? createPortal(
389
+ /* @__PURE__ */ React.createElement(
390
+ "div",
391
+ {
392
+ ref: listRef,
393
+ "data-density": density,
394
+ className: "aura-combo__popover aura-select__popover",
395
+ style: pos[0] ? {
396
+ left: pos[0].left,
397
+ minWidth: pos[0].width,
398
+ top: pos[0].top,
399
+ bottom: pos[0].bottom,
400
+ maxHeight: pos[0].maxHeight
401
+ } : { left: -9999, top: -9999 }
402
+ },
403
+ /* @__PURE__ */ React.createElement(
404
+ "div",
405
+ {
406
+ id: listId,
407
+ role: "listbox",
408
+ "aria-labelledby": labelledBy,
409
+ "aria-label": labelledBy ? void 0 : rest["aria-label"],
410
+ className: "aura-combo__list"
411
+ },
412
+ runs.map(function(r, n) {
413
+ if (r.group === null) return /* @__PURE__ */ React.createElement(React.Fragment, { key: "r" + n }, r.items.map(option));
414
+ const gid = id + "-group-" + n;
415
+ return /* @__PURE__ */ React.createElement("div", { key: "r" + n, role: "group", "aria-labelledby": gid }, /* @__PURE__ */ React.createElement("div", { id: gid, className: "aura-select__group" }, r.group), r.items.map(option));
416
+ })
417
+ )
418
+ ),
419
+ document.body
420
+ ) : null;
421
+ const userFocus = rest.onFocus, userBlur = rest.onBlur, userKey = rest.onKeyDown, userClick = rest.onClick;
422
+ return field(
423
+ /* @__PURE__ */ React.createElement(
424
+ "div",
425
+ {
426
+ className: cx("aura-input aura-select aura-select--custom", props.icon && "has-icon", open && "is-open"),
427
+ "data-invalid": props.error ? "" : void 0
428
+ },
429
+ props.icon ? /* @__PURE__ */ React.createElement(Icon, { name: props.icon, className: "aura-input__icon" }) : null,
430
+ h(
431
+ "select",
432
+ Object.assign(extra, pick(rest, FORM_KEYS), {
433
+ ref: selMerged,
434
+ id: id + "-select",
435
+ className: "aura-select__native",
436
+ tabIndex: -1,
437
+ "aria-hidden": true,
438
+ required: props.required,
439
+ onChange: function(e) {
440
+ sync();
441
+ if (props.onChange) props.onChange(e);
442
+ },
443
+ /* A browser focusing the <select> (the required bubble, a label click) hands focus to the button. */
444
+ onFocus: function() {
445
+ if (trigRef.current) trigRef.current.focus();
446
+ },
447
+ /* Only the blur the button reports (below): the <select>'s own, while it hands focus over, is not one. */
448
+ onBlur: function(e) {
449
+ if (!e.nativeEvent.isTrusted && userBlur) userBlur(e);
450
+ }
451
+ }),
452
+ opts,
453
+ props.children
454
+ ),
455
+ /* @__PURE__ */ React.createElement(
456
+ "button",
457
+ {
458
+ ...omit(rest, HANDLED_KEYS),
459
+ ref: trigRef,
460
+ type: "button",
461
+ role: "combobox",
462
+ form: id + "-no-form",
463
+ className: "aura-input__control aura-select__trigger",
464
+ disabled: props.disabled,
465
+ tabIndex: rest.tabIndex,
466
+ "aria-haspopup": "listbox",
467
+ "aria-expanded": open,
468
+ "aria-controls": open ? listId : void 0,
469
+ "aria-activedescendant": open && active >= 0 ? optId(active) : void 0,
470
+ id,
471
+ "aria-label": rest["aria-label"],
472
+ "aria-labelledby": rest["aria-labelledby"],
473
+ "aria-required": props.required || void 0,
474
+ "aria-invalid": props.error ? true : void 0,
475
+ "aria-describedby": described,
476
+ onFocus: function(e) {
477
+ if (userFocus) userFocus(e);
478
+ },
479
+ onClick: function(e) {
480
+ if (userClick) userClick(e);
481
+ if (e.defaultPrevented) return;
482
+ if (open) close(true);
483
+ else openList();
484
+ },
485
+ onKeyDown: function(e) {
486
+ if (userKey) userKey(e);
487
+ if (!e.defaultPrevented) onKeyDown(e);
488
+ },
489
+ onBlur: function(e) {
490
+ if (listRef.current && e.relatedTarget && listRef.current.contains(e.relatedTarget)) return;
491
+ if (open) close(false);
492
+ if (selRef.current) selRef.current.dispatchEvent(new FocusEvent("focusout", { bubbles: true }));
493
+ }
494
+ },
495
+ /* @__PURE__ */ React.createElement("span", { id: id + "-value", className: cx("aura-select__value", shown[0].empty && "is-placeholder") }, shown[0].label || " ")
496
+ ),
497
+ /* @__PURE__ */ React.createElement(Icon, { name: "chevron-down", className: "aura-select__chevron" }),
498
+ popup
499
+ )
44
500
  );
45
501
  });
46
502
  export {
package/dist/index.d.cts CHANGED
@@ -417,9 +417,12 @@ export type SelectOption = string | {
417
417
  label: string;
418
418
  disabled?: boolean | undefined;
419
419
  };
420
+ /** 5.3: opens AURA's own list (light and dark alike) over a real `<select>`, which keeps `name`, `required`, the ref,
421
+ * `onChange`, react-hook-form `register` and form posts. `multiple` or `size > 1` keeps the native list box. */
420
422
  export interface SelectProps extends FieldProps, Omit<React$1.SelectHTMLAttributes<HTMLSelectElement>, "required"> {
421
- options: SelectOption[];
422
- /** A disabled first option shown in fg-tertiary until something is chosen. */
423
+ /** The choices; or pass `<option>` / `<optgroup>` children instead (5.3: optional). */
424
+ options?: SelectOption[] | undefined;
425
+ /** Shown in fg-tertiary on the closed field until something is chosen; not listed as a choice. */
423
426
  placeholder?: string | undefined;
424
427
  icon?: IconInput | undefined;
425
428
  }
@@ -1217,6 +1220,7 @@ export interface FieldComponentProps extends Omit<FieldPropsPublic, "label"> {
1217
1220
  export declare const Field: React$1.ForwardRefExoticComponent<FieldComponentProps & React$1.RefAttributes<HTMLDivElement>>;
1218
1221
  export declare const TextField: React$1.ForwardRefExoticComponent<TextFieldProps & React$1.RefAttributes<HTMLInputElement>>;
1219
1222
  export declare const Textarea: React$1.ForwardRefExoticComponent<TextareaProps & React$1.RefAttributes<HTMLTextAreaElement>>;
1223
+ /** A select field: a button that opens an AURA list (5.3), over a real <select> that keeps forms, refs and events. */
1220
1224
  export declare const Select: React$1.ForwardRefExoticComponent<SelectProps & React$1.RefAttributes<HTMLSelectElement>>;
1221
1225
  export declare const RadioGroup: React$1.ForwardRefExoticComponent<RadioGroupProps & React$1.RefAttributes<HTMLFieldSetElement>>;
1222
1226
  export declare const Switch: React$1.ForwardRefExoticComponent<SwitchProps & React$1.RefAttributes<HTMLButtonElement>>;
package/dist/index.d.ts CHANGED
@@ -417,9 +417,12 @@ export type SelectOption = string | {
417
417
  label: string;
418
418
  disabled?: boolean | undefined;
419
419
  };
420
+ /** 5.3: opens AURA's own list (light and dark alike) over a real `<select>`, which keeps `name`, `required`, the ref,
421
+ * `onChange`, react-hook-form `register` and form posts. `multiple` or `size > 1` keeps the native list box. */
420
422
  export interface SelectProps extends FieldProps, Omit<React$1.SelectHTMLAttributes<HTMLSelectElement>, "required"> {
421
- options: SelectOption[];
422
- /** A disabled first option shown in fg-tertiary until something is chosen. */
423
+ /** The choices; or pass `<option>` / `<optgroup>` children instead (5.3: optional). */
424
+ options?: SelectOption[] | undefined;
425
+ /** Shown in fg-tertiary on the closed field until something is chosen; not listed as a choice. */
423
426
  placeholder?: string | undefined;
424
427
  icon?: IconInput | undefined;
425
428
  }
@@ -1217,6 +1220,7 @@ export interface FieldComponentProps extends Omit<FieldPropsPublic, "label"> {
1217
1220
  export declare const Field: React$1.ForwardRefExoticComponent<FieldComponentProps & React$1.RefAttributes<HTMLDivElement>>;
1218
1221
  export declare const TextField: React$1.ForwardRefExoticComponent<TextFieldProps & React$1.RefAttributes<HTMLInputElement>>;
1219
1222
  export declare const Textarea: React$1.ForwardRefExoticComponent<TextareaProps & React$1.RefAttributes<HTMLTextAreaElement>>;
1223
+ /** A select field: a button that opens an AURA list (5.3), over a real <select> that keeps forms, refs and events. */
1220
1224
  export declare const Select: React$1.ForwardRefExoticComponent<SelectProps & React$1.RefAttributes<HTMLSelectElement>>;
1221
1225
  export declare const RadioGroup: React$1.ForwardRefExoticComponent<RadioGroupProps & React$1.RefAttributes<HTMLFieldSetElement>>;
1222
1226
  export declare const Switch: React$1.ForwardRefExoticComponent<SwitchProps & React$1.RefAttributes<HTMLButtonElement>>;
package/dist/styles.css CHANGED
@@ -424,7 +424,16 @@ a.aura-icon-btn { text-decoration: none; }
424
424
  .aura-select .aura-input__control { padding-right: calc(var(--aura-space-3) + var(--aura-icon-sm) + var(--aura-space-2)); cursor: pointer; }
425
425
  .aura-select__chevron { position: absolute; right: var(--aura-space-3); color: var(--aura-fg-secondary); pointer-events: none; }
426
426
  .aura-select select:invalid, .aura-select select:has(option[value=""]:checked) { color: var(--aura-fg-tertiary); }
427
+ /* 5.3: the field is a button over an invisible <select> (forms, refs, events); the list is AURA's, like Combobox. */
427
428
  .aura-select option { color: var(--aura-fg-primary); background: var(--aura-bg-input); }
429
+ .aura-select--custom { position: relative; }
430
+ .aura-select__native { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; padding: 0; border: 0; opacity: 0; pointer-events: none; appearance: none; }
431
+ .aura-select__trigger { display: flex; align-items: center; text-align: start; background: none; border: 0; color: inherit; font: inherit; width: 100%; min-width: 0; }
432
+ .aura-select__trigger:disabled { cursor: not-allowed; }
433
+ .aura-select__value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
434
+ .aura-select__value.is-placeholder { color: var(--aura-fg-tertiary); }
435
+ .aura-select__group { padding: var(--aura-space-2) var(--aura-space-2) var(--aura-space-1); font-family: var(--font-mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--aura-fg-tertiary); }
436
+ .aura-select.is-open .aura-select__chevron { transform: rotate(180deg); }
428
437
 
429
438
  .aura-radio-group > legend { margin-bottom: 6px; }
430
439
  .aura-radio-group__list { display: flex; flex-direction: column; gap: var(--aura-space-3); }
@@ -426,7 +426,16 @@ a.aura-icon-btn { text-decoration: none; }
426
426
  .aura-select .aura-input__control { padding-right: calc(var(--aura-space-3) + var(--aura-icon-sm) + var(--aura-space-2)); cursor: pointer; }
427
427
  .aura-select__chevron { position: absolute; right: var(--aura-space-3); color: var(--aura-fg-secondary); pointer-events: none; }
428
428
  .aura-select select:invalid, .aura-select select:has(option[value=""]:checked) { color: var(--aura-fg-tertiary); }
429
+ /* 5.3: the field is a button over an invisible <select> (forms, refs, events); the list is AURA's, like Combobox. */
429
430
  .aura-select option { color: var(--aura-fg-primary); background: var(--aura-bg-input); }
431
+ .aura-select--custom { position: relative; }
432
+ .aura-select__native { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; padding: 0; border: 0; opacity: 0; pointer-events: none; appearance: none; }
433
+ .aura-select__trigger { display: flex; align-items: center; text-align: start; background: none; border: 0; color: inherit; font: inherit; width: 100%; min-width: 0; }
434
+ .aura-select__trigger:disabled { cursor: not-allowed; }
435
+ .aura-select__value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
436
+ .aura-select__value.is-placeholder { color: var(--aura-fg-tertiary); }
437
+ .aura-select__group { padding: var(--aura-space-2) var(--aura-space-2) var(--aura-space-1); font-family: var(--font-mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--aura-fg-tertiary); }
438
+ .aura-select.is-open .aura-select__chevron { transform: rotate(180deg); }
430
439
 
431
440
  .aura-radio-group > legend { margin-bottom: 6px; }
432
441
  .aura-radio-group__list { display: flex; flex-direction: column; gap: var(--aura-space-3); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jirawatpyk/aura-react",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "AURA Design System — React components (Enterprise Standard, Human Creative). Thai-first: Buddhist-era dates, Thai collation.",
5
5
  "license": "MIT",
6
6
  "type": "module",