@octaviaflow/core 3.0.5 → 3.0.7

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/index.js CHANGED
@@ -607,6 +607,32 @@ function Breadcrumb({
607
607
  import { motion as motion2 } from "framer-motion";
608
608
  import { useRef as useRef3 } from "react";
609
609
  import { useButton } from "react-aria";
610
+
611
+ // src/utils/a11y.ts
612
+ function resolveAccessibleName(input) {
613
+ if (input.ariaLabelledby) {
614
+ return { "aria-labelledby": input.ariaLabelledby };
615
+ }
616
+ if (input.ariaLabel) {
617
+ return { "aria-label": input.ariaLabel };
618
+ }
619
+ if (typeof input.label === "string" && input.label.trim().length > 0) {
620
+ return { "aria-label": input.label };
621
+ }
622
+ for (const candidate of input.fallbacks ?? []) {
623
+ if (typeof candidate === "string" && candidate.trim().length > 0) {
624
+ return { "aria-label": candidate };
625
+ }
626
+ }
627
+ if (process.env.NODE_ENV !== "production") {
628
+ console.error(
629
+ `[@octaviaflow/core ${input.componentName}] No accessible name. Pass a string \`label\`, or \`aria-label\`, or \`aria-labelledby\` so screen readers can announce this control.`
630
+ );
631
+ }
632
+ return { "aria-label": `Unlabeled ${input.componentName}` };
633
+ }
634
+
635
+ // src/components/Button/Button.tsx
610
636
  import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
611
637
  function Button({
612
638
  variant = "primary",
@@ -624,11 +650,19 @@ function Button({
624
650
  const ref = useRef3(null);
625
651
  const isDisabled = disabled || loading;
626
652
  const resolvedType = type ?? "button";
653
+ const hasVisibleText = typeof children === "string" ? children.trim().length > 0 : Boolean(children);
654
+ const needsAriaName = !hasVisibleText;
655
+ const ariaNameProps = needsAriaName ? resolveAccessibleName({
656
+ ariaLabel: props["aria-label"],
657
+ ariaLabelledby: props["aria-labelledby"],
658
+ componentName: "Button"
659
+ }) : void 0;
627
660
  const { buttonProps } = useButton(
628
661
  {
629
662
  isDisabled,
630
663
  onPress: props.onClick,
631
- type: resolvedType
664
+ type: resolvedType,
665
+ ...ariaNameProps ?? {}
632
666
  },
633
667
  ref
634
668
  );
@@ -644,7 +678,7 @@ function Button({
644
678
  onBlur: _onBlur,
645
679
  ...passthroughProps
646
680
  } = props;
647
- return /* @__PURE__ */ jsxs10(
681
+ return /* @__PURE__ */ jsx10(
648
682
  motion2.button,
649
683
  {
650
684
  ...passthroughProps,
@@ -662,27 +696,32 @@ function Button({
662
696
  "data-loading": loading || void 0,
663
697
  whileTap: isDisabled ? void 0 : { scale: 0.97 },
664
698
  transition: { duration: 0.1 },
665
- children: [
666
- /* @__PURE__ */ jsxs10("span", { className: "ods-btn__content", "aria-hidden": loading, children: [
667
- leftIcon && /* @__PURE__ */ jsx10("span", { className: "ods-btn__icon ods-btn__icon--left", children: leftIcon }),
668
- children && /* @__PURE__ */ jsx10("span", { className: "ods-btn__label", children }),
669
- rightIcon && /* @__PURE__ */ jsx10("span", { className: "ods-btn__icon ods-btn__icon--right", children: rightIcon })
670
- ] }),
671
- loading && /* @__PURE__ */ jsx10("span", { className: "ods-btn__spinner", role: "status", "aria-label": "Loading", children: /* @__PURE__ */ jsx10("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", children: /* @__PURE__ */ jsx10(
672
- "circle",
699
+ children: /* @__PURE__ */ jsxs10("span", { className: "ods-btn__content", children: [
700
+ loading ? /* @__PURE__ */ jsx10(
701
+ "span",
673
702
  {
674
- cx: "12",
675
- cy: "12",
676
- r: "10",
677
- fill: "none",
678
- stroke: "currentColor",
679
- strokeWidth: "2.5",
680
- strokeLinecap: "round",
681
- strokeDasharray: "32",
682
- strokeDashoffset: "12"
703
+ className: "ods-btn__icon ods-btn__icon--left ods-btn__icon--spinner",
704
+ role: "status",
705
+ "aria-label": "Loading",
706
+ children: /* @__PURE__ */ jsx10("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", children: /* @__PURE__ */ jsx10(
707
+ "circle",
708
+ {
709
+ cx: "12",
710
+ cy: "12",
711
+ r: "10",
712
+ fill: "none",
713
+ stroke: "currentColor",
714
+ strokeWidth: "2.5",
715
+ strokeLinecap: "round",
716
+ strokeDasharray: "32",
717
+ strokeDashoffset: "12"
718
+ }
719
+ ) })
683
720
  }
684
- ) }) })
685
- ]
721
+ ) : leftIcon && /* @__PURE__ */ jsx10("span", { className: "ods-btn__icon ods-btn__icon--left", children: leftIcon }),
722
+ children && /* @__PURE__ */ jsx10("span", { className: "ods-btn__label", children }),
723
+ rightIcon && !loading && /* @__PURE__ */ jsx10("span", { className: "ods-btn__icon ods-btn__icon--right", children: rightIcon })
724
+ ] })
686
725
  }
687
726
  );
688
727
  }
@@ -2454,13 +2493,19 @@ function Checkbox({
2454
2493
  defaultSelected: defaultChecked,
2455
2494
  onChange
2456
2495
  });
2496
+ const ariaNameProps = resolveAccessibleName({
2497
+ label,
2498
+ ariaLabel: props["aria-label"],
2499
+ ariaLabelledby: props["aria-labelledby"],
2500
+ componentName: "Checkbox"
2501
+ });
2457
2502
  const { inputProps } = useCheckbox(
2458
2503
  {
2459
2504
  isSelected: state.isSelected,
2460
2505
  isIndeterminate: indeterminate,
2461
2506
  isDisabled: disabled,
2462
2507
  onChange,
2463
- "aria-label": typeof label === "string" ? label : void 0
2508
+ ...ariaNameProps
2464
2509
  },
2465
2510
  state,
2466
2511
  ref
@@ -6363,11 +6408,27 @@ function MenuPopup({
6363
6408
  }
6364
6409
  return null;
6365
6410
  }
6366
- function DropdownMenu({ trigger, items, align = "start", className }) {
6411
+ function DropdownMenu({
6412
+ trigger,
6413
+ items,
6414
+ align = "start",
6415
+ className,
6416
+ "aria-label": ariaLabel,
6417
+ "aria-labelledby": ariaLabelledby
6418
+ }) {
6367
6419
  const triggerRef = useRef12(null);
6368
6420
  const state = $e3403870bfb691da$export$79fefeb1c2091ac3({});
6369
6421
  const { menuTriggerProps } = useMenuTrigger({}, state, triggerRef);
6370
- const { buttonProps } = useButton3(menuTriggerProps, triggerRef);
6422
+ const ariaNameProps = resolveAccessibleName({
6423
+ ariaLabel,
6424
+ ariaLabelledby,
6425
+ componentName: "DropdownMenu",
6426
+ fallbacks: [typeof trigger === "string" ? trigger : void 0]
6427
+ });
6428
+ const { buttonProps } = useButton3(
6429
+ { ...menuTriggerProps, ...ariaNameProps },
6430
+ triggerRef
6431
+ );
6371
6432
  const { onDrag, onDragStart, onDragEnd, onAnimationStart, ...safeTriggerProps } = buttonProps;
6372
6433
  return /* @__PURE__ */ jsxs28(Fragment6, { children: [
6373
6434
  /* @__PURE__ */ jsx29("button", { ...safeTriggerProps, ref: triggerRef, className: "ods-dropdown__trigger", children: trigger }),
@@ -10181,11 +10242,18 @@ function Radio({
10181
10242
  }) {
10182
10243
  const state = useRadioGroupContext();
10183
10244
  const ref = useRef24(null);
10245
+ const ariaNameProps = resolveAccessibleName({
10246
+ label,
10247
+ ariaLabel: props["aria-label"],
10248
+ ariaLabelledby: props["aria-labelledby"],
10249
+ componentName: "Radio",
10250
+ fallbacks: [value]
10251
+ });
10184
10252
  const { inputProps } = useRadio(
10185
10253
  {
10186
10254
  value,
10187
10255
  isDisabled: disabled,
10188
- "aria-label": label
10256
+ ...ariaNameProps
10189
10257
  },
10190
10258
  state,
10191
10259
  ref
@@ -10783,6 +10851,15 @@ function Select({
10783
10851
  (o) => o.label.toLowerCase().includes(q) || o.description?.toLowerCase().includes(q)
10784
10852
  );
10785
10853
  }, [options, searchQuery, searchable]);
10854
+ const ariaNameProps = useMemo10(
10855
+ () => resolveAccessibleName({
10856
+ label,
10857
+ ariaLabel,
10858
+ ariaLabelledby: ariaLabelledBy,
10859
+ componentName: "Select"
10860
+ }),
10861
+ [label, ariaLabel, ariaLabelledBy]
10862
+ );
10786
10863
  const ariaProps = useMemo10(() => {
10787
10864
  const items = filteredOptions.map((o) => ({
10788
10865
  key: o.value,
@@ -10791,7 +10868,10 @@ function Select({
10791
10868
  isDisabled: o.disabled
10792
10869
  }));
10793
10870
  const props = {
10794
- label: label || "Select",
10871
+ // Visible string label when present; otherwise rely on the aria-*
10872
+ // props for screen-reader name.
10873
+ label: typeof label === "string" ? label : void 0,
10874
+ ...ariaNameProps,
10795
10875
  items,
10796
10876
  children: (item) => /* @__PURE__ */ jsx66($05678f3aee5e7d1a$export$6d08773d2e66f8f2, { textValue: item.label, children: item.label }, item.key),
10797
10877
  isDisabled: disabled,
@@ -10807,7 +10887,7 @@ function Select({
10807
10887
  props.defaultSelectedKey = defaultValue;
10808
10888
  }
10809
10889
  return props;
10810
- }, [filteredOptions, label, disabled, value, defaultValue, onChange]);
10890
+ }, [filteredOptions, label, disabled, value, defaultValue, onChange, ariaNameProps]);
10811
10891
  const state = $29256f53a2edafe9$export$5159ec8b34d4ec12(ariaProps);
10812
10892
  const { triggerProps, menuProps } = useSelect(ariaProps, state, triggerRef);
10813
10893
  const { buttonProps } = useButton4(triggerProps, triggerRef);
@@ -12166,12 +12246,18 @@ function Switch({
12166
12246
  defaultSelected: defaultChecked,
12167
12247
  onChange
12168
12248
  });
12249
+ const ariaNameProps = resolveAccessibleName({
12250
+ label,
12251
+ ariaLabel: props["aria-label"],
12252
+ ariaLabelledby: props["aria-labelledby"],
12253
+ componentName: "Switch"
12254
+ });
12169
12255
  const { inputProps } = useSwitch(
12170
12256
  {
12171
12257
  isSelected: state.isSelected,
12172
12258
  isDisabled: disabled,
12173
12259
  onChange,
12174
- "aria-label": typeof label === "string" ? label : void 0
12260
+ ...ariaNameProps
12175
12261
  },
12176
12262
  state,
12177
12263
  ref
@@ -12617,9 +12703,18 @@ function Textarea({
12617
12703
  const ref = useRef32(null);
12618
12704
  const errorId = useId4();
12619
12705
  const [charCount, setCharCount] = useState26(() => String(value ?? defaultValue ?? "").length);
12706
+ const ariaNameProps = resolveAccessibleName({
12707
+ label,
12708
+ ariaLabel: props["aria-label"],
12709
+ ariaLabelledby: props["aria-labelledby"],
12710
+ componentName: "Textarea"
12711
+ });
12620
12712
  const { labelProps, inputProps } = useTextField2(
12621
12713
  {
12622
- label: label || props["aria-label"] || "textarea",
12714
+ // Pass a string `label` only when caller gave a string; otherwise rely
12715
+ // on the aria-* props from resolveAccessibleName.
12716
+ label: typeof label === "string" ? label : void 0,
12717
+ ...ariaNameProps,
12623
12718
  isDisabled: disabled,
12624
12719
  errorMessage,
12625
12720
  validationState: error ? "invalid" : void 0,