@jirawatpyk/aura-react 5.29.0 → 5.30.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/dist/esm/Menu.js CHANGED
@@ -6,6 +6,39 @@ import { Icon } from "./Icon.js";
6
6
  import { useLinkComponent } from "./locale.js";
7
7
  import { IconCheck } from "./icons.js";
8
8
  const ITEMS = '[role^="menuitem"]';
9
+ function makesBlock(s) {
10
+ return s.transform !== "none" || s.translate !== "none" || s.rotate !== "none" || s.scale !== "none" || s.filter !== "none" || s.backdropFilter !== "none" || s.perspective !== "none" || s.contentVisibility === "auto" || /paint|layout|strict|content/.test(s.contain) || /transform|translate|rotate|scale|filter|perspective/.test(s.willChange);
11
+ }
12
+ function inView(el) {
13
+ if (!el.isConnected) return false;
14
+ const r = el.getBoundingClientRect();
15
+ if (r.width === 0 && r.height === 0) return false;
16
+ let top = r.top, bottom = r.bottom, left = r.left, right = r.right;
17
+ let pos = getComputedStyle(el).position;
18
+ for (let p = el.parentElement; p && p !== document.body && p !== document.documentElement; p = p.parentElement) {
19
+ const s = getComputedStyle(p);
20
+ const block = makesBlock(s);
21
+ if (pos === "fixed" && !block) continue;
22
+ if (pos === "absolute" && s.position === "static" && !block) continue;
23
+ if (s.display !== "contents") {
24
+ const b = p.getBoundingClientRect();
25
+ if (s.overflowX !== "visible") {
26
+ left = Math.max(left, b.left);
27
+ right = Math.min(right, b.right);
28
+ }
29
+ if (s.overflowY !== "visible") {
30
+ top = Math.max(top, b.top);
31
+ bottom = Math.min(bottom, b.bottom);
32
+ }
33
+ }
34
+ pos = s.position === "fixed" || s.position === "absolute" ? s.position : "static";
35
+ }
36
+ top = Math.max(top, 0);
37
+ left = Math.max(left, 0);
38
+ bottom = Math.min(bottom, window.innerHeight);
39
+ right = Math.min(right, window.innerWidth);
40
+ return bottom > top && right > left;
41
+ }
9
42
  const Menu = React.forwardRef(function Menu2(props, ref) {
10
43
  const own = React.useRef(null), merged = useMergedRef(ref, own);
11
44
  const posState = React.useState(null);
@@ -15,21 +48,30 @@ const Menu = React.forwardRef(function Menu2(props, ref) {
15
48
  const headerId = uid(), itemId = uid();
16
49
  const hasHeader = props.header != null && props.header !== false && props.header !== "";
17
50
  const Link = useLinkComponent(props.linkComponent);
51
+ function place() {
52
+ const a = props.anchor, m = own.current;
53
+ if (!a || !m) return;
54
+ const r = a.getBoundingClientRect(), mh = m.scrollHeight, mw = m.offsetWidth, below = window.innerHeight - r.bottom - 12, above = r.top - 12;
55
+ let top = r.bottom + 4, maxHeight;
56
+ if (mh > below) {
57
+ if (mh <= above) top = r.top - mh - 4;
58
+ else if (above > below) {
59
+ maxHeight = above;
60
+ top = r.top - above - 4;
61
+ } else maxHeight = below;
62
+ }
63
+ const left = Math.max(8, Math.min(r.right - mw, window.innerWidth - mw - 8));
64
+ setPos(function(p) {
65
+ return p && p.top === top && p.left === left && p.maxHeight === maxHeight ? p : { top, left, maxHeight };
66
+ });
67
+ }
68
+ const placeRef = React.useRef(place);
69
+ placeRef.current = place;
70
+ const live = React.useRef({ anchor: props.anchor, onClose: props.onClose });
71
+ live.current = { anchor: props.anchor, onClose: props.onClose };
18
72
  useIsoLayoutEffect(
19
73
  function() {
20
- const a = props.anchor, m = own.current;
21
- if (!a || !m) return;
22
- const r = a.getBoundingClientRect(), mh = m.scrollHeight, mw = m.offsetWidth, below = window.innerHeight - r.bottom - 12, above = r.top - 12;
23
- let top = r.bottom + 4, maxHeight;
24
- if (mh > below) {
25
- if (mh <= above) top = r.top - mh - 4;
26
- else if (above > below) {
27
- maxHeight = above;
28
- top = r.top - above - 4;
29
- } else maxHeight = below;
30
- }
31
- const left = Math.max(8, Math.min(r.right - mw, window.innerWidth - mw - 8));
32
- setPos({ top, left, maxHeight });
74
+ place();
33
75
  },
34
76
  [props.anchor, mounted, hasHeader]
35
77
  );
@@ -59,7 +101,12 @@ const Menu = React.forwardRef(function Menu2(props, ref) {
59
101
  props.onClose(false);
60
102
  }
61
103
  function onScroll(e) {
62
- if (own.current && !own.current.contains(e.target)) props.onClose(false);
104
+ if (own.current && e.target instanceof Node && own.current.contains(e.target)) return;
105
+ const a = live.current.anchor;
106
+ if (a && !inView(a)) {
107
+ live.current.onClose(false);
108
+ if (a instanceof HTMLElement && a.closest('[aria-modal="true"]')) a.focus({ preventScroll: true });
109
+ } else placeRef.current();
63
110
  }
64
111
  document.addEventListener("pointerdown", outside, true);
65
112
  window.addEventListener("scroll", onScroll, true);
@@ -2,7 +2,7 @@
2
2
  import * as React from "react";
3
3
  import { describedBy } from "./Field.js";
4
4
  import { Icon } from "./Icon.js";
5
- import { cx, uid, useMaybeControlled } from "./internal.js";
5
+ import { cx, touchClass, uid, useMaybeControlled } from "./internal.js";
6
6
  import { IconCircleAlert } from "./icons.js";
7
7
  const RadioGroup = React.forwardRef(function RadioGroup2(props, ref) {
8
8
  const auto = uid(), id = props.id || auto;
@@ -28,7 +28,12 @@ const RadioGroup = React.forwardRef(function RadioGroup2(props, ref) {
28
28
  "fieldset",
29
29
  {
30
30
  ref: setRef,
31
- className: cx("aura-field aura-radio-group", props.error && "is-invalid", props.className),
31
+ className: cx(
32
+ "aura-field aura-radio-group",
33
+ props.error && "is-invalid",
34
+ touchClass(props.touchHeight),
35
+ props.className
36
+ ),
32
37
  "aria-describedby": describedBy(id, props),
33
38
  "aria-invalid": props.error ? true : void 0,
34
39
  disabled: props.disabled
@@ -4,7 +4,7 @@ import { createPortal } from "react-dom";
4
4
  import { FIELD_KEYS, Field, describedBy } from "./Field.js";
5
5
  import { Icon } from "./Icon.js";
6
6
  import { useDensity } from "./locale.js";
7
- import { cx, devWarnOnce, omit, uid, useIsoLayoutEffect, useMergedRef, useMounted } from "./internal.js";
7
+ import { cx, devWarnOnce, omit, touchClass, uid, useIsoLayoutEffect, useMergedRef, useMounted } from "./internal.js";
8
8
  import { IconCheck, IconChevronDown } from "./icons.js";
9
9
  const h = React.createElement;
10
10
  const FILTER_KEY = "__auraFilter";
@@ -418,7 +418,7 @@ const Select = React.forwardRef(function Select2(props, ref) {
418
418
  required: props.required,
419
419
  optional: props.optional,
420
420
  disabled: props.disabled,
421
- className: props.className
421
+ className: cx(touchClass(props.touchHeight), props.className)
422
422
  },
423
423
  child
424
424
  );
package/dist/esm/Tag.js CHANGED
@@ -90,7 +90,7 @@ const Tag = React.forwardRef(function Tag2(props, ref) {
90
90
  "aria-label": props.removeLabel || t.remove(name),
91
91
  onClick: props.onRemove
92
92
  },
93
- /* @__PURE__ */ React.createElement(Icon, { name: /* @__PURE__ */ React.createElement(IconX, null), size: 12 })
93
+ /* @__PURE__ */ React.createElement(Icon, { name: /* @__PURE__ */ React.createElement(IconX, null), size: 14 })
94
94
  );
95
95
  return /* @__PURE__ */ React.createElement(Tooltip, { content: fullText, open: fullText ? void 0 : false }, btn);
96
96
  }() : null);
@@ -2,7 +2,7 @@
2
2
  import * as React from "react";
3
3
  import { FIELD_KEYS, Field, describedBy } from "./Field.js";
4
4
  import { Icon } from "./Icon.js";
5
- import { cx, omit, uid } from "./internal.js";
5
+ import { cx, omit, touchClass, uid } from "./internal.js";
6
6
  const TextField = React.forwardRef(function TextField2(props, ref) {
7
7
  const auto = uid(), id = props.id || auto;
8
8
  const rest = omit(props, FIELD_KEYS);
@@ -16,7 +16,7 @@ const TextField = React.forwardRef(function TextField2(props, ref) {
16
16
  required: props.required,
17
17
  optional: props.optional,
18
18
  disabled: props.disabled,
19
- className: props.className
19
+ className: cx(touchClass(props.touchHeight), props.className)
20
20
  },
21
21
  /* @__PURE__ */ React.createElement("div", { className: cx("aura-input", props.icon && "has-icon", props.suffix && "has-suffix") }, props.icon ? /* @__PURE__ */ React.createElement(Icon, { name: props.icon, className: "aura-input__icon" }) : null, /* @__PURE__ */ React.createElement(
22
22
  "input",
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import * as React from "react";
3
3
  import { FIELD_KEYS, Field, describedBy } from "./Field.js";
4
- import { omit, uid } from "./internal.js";
4
+ import { cx, omit, touchClass, uid } from "./internal.js";
5
5
  const Textarea = React.forwardRef(function Textarea2(props, ref) {
6
6
  const auto = uid(), id = props.id || auto;
7
7
  const rest = omit(props, FIELD_KEYS);
@@ -15,7 +15,7 @@ const Textarea = React.forwardRef(function Textarea2(props, ref) {
15
15
  required: props.required,
16
16
  optional: props.optional,
17
17
  disabled: props.disabled,
18
- className: props.className
18
+ className: cx(touchClass(props.touchHeight), props.className)
19
19
  },
20
20
  /* @__PURE__ */ React.createElement(
21
21
  "textarea",
@@ -8,6 +8,9 @@ function omit(src, keys) {
8
8
  out[k] = src[k];
9
9
  return out;
10
10
  }
11
+ function touchClass(t) {
12
+ return t === "always" ? "is-touch-always" : !!t && "is-touch";
13
+ }
11
14
  const TONES = ["neutral", "accent", "success", "warning", "danger"];
12
15
  function tone(t) {
13
16
  return TONES.indexOf(t) >= 0 ? t : "neutral";
@@ -29,5 +32,6 @@ export {
29
32
  cx,
30
33
  devWarnOnce,
31
34
  omit,
32
- tone
35
+ tone,
36
+ touchClass
33
37
  };
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import * as React from "react";
3
- import { cx, omit, tone, devWarnOnce } from "./classes.js";
3
+ import { cx, omit, tone, devWarnOnce, touchClass } from "./classes.js";
4
4
  function useMaybeControlled(value, initial, onChange) {
5
5
  const s = React.useState(initial);
6
6
  const wasSet = React.useState(value !== void 0);
@@ -89,6 +89,7 @@ export {
89
89
  omit,
90
90
  plainClick,
91
91
  tone,
92
+ touchClass,
92
93
  trapTab,
93
94
  uid,
94
95
  useIsoLayoutEffect,
package/dist/index.d.cts CHANGED
@@ -335,6 +335,9 @@ export interface CheckboxProps extends Omit<React$1.InputHTMLAttributes<HTMLInpu
335
335
  x: number;
336
336
  y: number;
337
337
  } | undefined;
338
+ /** A labelled checkbox's row is 44px (5.30, Chamber-OS 135), the one-line label centred: `true` below 640px or on a
339
+ * coarse pointer, `'always'` at every width. A bare box (no visible label) uses `hitArea` instead. */
340
+ touchHeight?: TouchHeight | undefined;
338
341
  }
339
342
  /** 32px round button holding one icon. */
340
343
  export interface IconButtonProps extends React$1.ButtonHTMLAttributes<HTMLButtonElement> {
@@ -393,6 +396,8 @@ export interface MenuProps {
393
396
  * accessible description, read once when the menu opens. */
394
397
  header?: React$1.ReactNode | undefined;
395
398
  }
399
+ /** `true`: 44px below 640px or on a coarse pointer; `'always'`: 44px at every width (5.30). */
400
+ export type TouchHeight = boolean | "always";
396
401
  export interface FieldProps {
397
402
  /** Visible label; also the accessible name. */
398
403
  label: string;
@@ -410,6 +415,10 @@ export interface TextFieldProps extends FieldProps, Omit<React$1.InputHTMLAttrib
410
415
  icon?: IconInput | undefined;
411
416
  /** Trailing unit, e.g. "THB". */
412
417
  suffix?: React$1.ReactNode | undefined;
418
+ /** 44px box (5.30, Chamber-OS 135): `true` below 640px or on a coarse pointer, as Button's `touchHeight`; `'always'`
419
+ * at every width, also with a mouse in compact density — for a form whose spec asks for 44px targets everywhere.
420
+ * Default: the density's input height. */
421
+ touchHeight?: TouchHeight | undefined;
413
422
  }
414
423
  /** A password input with a show/hide button. All TextField props; `ref` reaches the input (react-hook-form `register`). */
415
424
  export interface PasswordFieldProps extends Omit<TextFieldProps, "type" | "suffix"> {
@@ -531,6 +540,9 @@ export interface CommandProps {
531
540
  className?: string | undefined;
532
541
  }
533
542
  export interface TextareaProps extends FieldProps, Omit<React$1.TextareaHTMLAttributes<HTMLTextAreaElement>, "required"> {
543
+ /** Accepted like the other fields' (5.30, Chamber-OS 135), so a form can pass it to every field. A Textarea is
544
+ * always at least 96px tall, well over the 44px target, so it changes nothing you can see. */
545
+ touchHeight?: TouchHeight | undefined;
534
546
  }
535
547
  export type SelectOption = string | {
536
548
  value: string;
@@ -552,6 +564,10 @@ export interface SelectProps extends Omit<FieldProps, "label">, Omit<React$1.Sel
552
564
  * option on the read-only ground, with no chevron; no click or key opens the list. The value is still submitted
553
565
  * with the form (unlike `disabled`). Before JavaScript runs, the other options from `options` are disabled. */
554
566
  readOnly?: boolean | undefined;
567
+ /** 44px box (5.30, Chamber-OS 135): `true` below 640px or on a coarse pointer, as Button's `touchHeight`; `'always'`
568
+ * at every width, also with a mouse in compact density — for a form whose spec asks for 44px targets everywhere.
569
+ * Default: the density's input height. */
570
+ touchHeight?: TouchHeight | undefined;
555
571
  }
556
572
  /** A compact filter for FilterBar (5.12): "Status All ▾", as wide as its words. */
557
573
  export interface FilterSelectProps extends Omit<React$1.SelectHTMLAttributes<HTMLSelectElement>, "onChange" | "value" | "defaultValue" | "multiple" | "size" | "placeholder" | "required" | "children"> {
@@ -586,6 +602,9 @@ export interface RadioGroupProps extends FieldProps {
586
602
  disabled?: boolean | undefined;
587
603
  id?: string | undefined;
588
604
  className?: string | undefined;
605
+ /** 44px rows (5.30, Chamber-OS 135), the one-line label centred: `true` below 640px or on a coarse pointer (touch
606
+ * screens already get it), `'always'` at every width, also with a mouse. Default: as tall as the control and label. */
607
+ touchHeight?: TouchHeight | undefined;
589
608
  }
590
609
  export interface SwitchProps {
591
610
  label?: string | undefined;
package/dist/index.d.ts CHANGED
@@ -335,6 +335,9 @@ export interface CheckboxProps extends Omit<React$1.InputHTMLAttributes<HTMLInpu
335
335
  x: number;
336
336
  y: number;
337
337
  } | undefined;
338
+ /** A labelled checkbox's row is 44px (5.30, Chamber-OS 135), the one-line label centred: `true` below 640px or on a
339
+ * coarse pointer, `'always'` at every width. A bare box (no visible label) uses `hitArea` instead. */
340
+ touchHeight?: TouchHeight | undefined;
338
341
  }
339
342
  /** 32px round button holding one icon. */
340
343
  export interface IconButtonProps extends React$1.ButtonHTMLAttributes<HTMLButtonElement> {
@@ -393,6 +396,8 @@ export interface MenuProps {
393
396
  * accessible description, read once when the menu opens. */
394
397
  header?: React$1.ReactNode | undefined;
395
398
  }
399
+ /** `true`: 44px below 640px or on a coarse pointer; `'always'`: 44px at every width (5.30). */
400
+ export type TouchHeight = boolean | "always";
396
401
  export interface FieldProps {
397
402
  /** Visible label; also the accessible name. */
398
403
  label: string;
@@ -410,6 +415,10 @@ export interface TextFieldProps extends FieldProps, Omit<React$1.InputHTMLAttrib
410
415
  icon?: IconInput | undefined;
411
416
  /** Trailing unit, e.g. "THB". */
412
417
  suffix?: React$1.ReactNode | undefined;
418
+ /** 44px box (5.30, Chamber-OS 135): `true` below 640px or on a coarse pointer, as Button's `touchHeight`; `'always'`
419
+ * at every width, also with a mouse in compact density — for a form whose spec asks for 44px targets everywhere.
420
+ * Default: the density's input height. */
421
+ touchHeight?: TouchHeight | undefined;
413
422
  }
414
423
  /** A password input with a show/hide button. All TextField props; `ref` reaches the input (react-hook-form `register`). */
415
424
  export interface PasswordFieldProps extends Omit<TextFieldProps, "type" | "suffix"> {
@@ -531,6 +540,9 @@ export interface CommandProps {
531
540
  className?: string | undefined;
532
541
  }
533
542
  export interface TextareaProps extends FieldProps, Omit<React$1.TextareaHTMLAttributes<HTMLTextAreaElement>, "required"> {
543
+ /** Accepted like the other fields' (5.30, Chamber-OS 135), so a form can pass it to every field. A Textarea is
544
+ * always at least 96px tall, well over the 44px target, so it changes nothing you can see. */
545
+ touchHeight?: TouchHeight | undefined;
534
546
  }
535
547
  export type SelectOption = string | {
536
548
  value: string;
@@ -552,6 +564,10 @@ export interface SelectProps extends Omit<FieldProps, "label">, Omit<React$1.Sel
552
564
  * option on the read-only ground, with no chevron; no click or key opens the list. The value is still submitted
553
565
  * with the form (unlike `disabled`). Before JavaScript runs, the other options from `options` are disabled. */
554
566
  readOnly?: boolean | undefined;
567
+ /** 44px box (5.30, Chamber-OS 135): `true` below 640px or on a coarse pointer, as Button's `touchHeight`; `'always'`
568
+ * at every width, also with a mouse in compact density — for a form whose spec asks for 44px targets everywhere.
569
+ * Default: the density's input height. */
570
+ touchHeight?: TouchHeight | undefined;
555
571
  }
556
572
  /** A compact filter for FilterBar (5.12): "Status All ▾", as wide as its words. */
557
573
  export interface FilterSelectProps extends Omit<React$1.SelectHTMLAttributes<HTMLSelectElement>, "onChange" | "value" | "defaultValue" | "multiple" | "size" | "placeholder" | "required" | "children"> {
@@ -586,6 +602,9 @@ export interface RadioGroupProps extends FieldProps {
586
602
  disabled?: boolean | undefined;
587
603
  id?: string | undefined;
588
604
  className?: string | undefined;
605
+ /** 44px rows (5.30, Chamber-OS 135), the one-line label centred: `true` below 640px or on a coarse pointer (touch
606
+ * screens already get it), `'always'` at every width, also with a mouse. Default: as tall as the control and label. */
607
+ touchHeight?: TouchHeight | undefined;
589
608
  }
590
609
  export interface SwitchProps {
591
610
  label?: string | undefined;