@tonycodes-ui/mortar 0.12.0 → 0.13.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/src/marketing.tsx CHANGED
@@ -3,17 +3,9 @@ import {
3
3
  KitLink,
4
4
  type IconLink,
5
5
  type ItemGroup,
6
- type LinkItem,
7
6
  } from "./navigation.js";
8
7
  import { useId, useState, type CSSProperties, type ReactNode } from "react";
9
- import {
10
- CloseButton,
11
- DialogPanel,
12
- DialogTitle,
13
- Popover,
14
- PopoverButton,
15
- PopoverPanel,
16
- } from "@headlessui/react";
8
+ import { DialogPanel, DialogTitle } from "@headlessui/react";
17
9
  import { ActionLink, Button, type LinkAction } from "./primitives.js";
18
10
  import { BrandMark, Prose, type SectionTone } from "./layout.js";
19
11
  import { SegmentedControl } from "./inspection.js";
@@ -23,6 +15,8 @@ import { childLevel, headingTag, type HeadingLevel } from "./heading.js";
23
15
  import {
24
16
  ActionLinks,
25
17
  DialogFrame,
18
+ Flyout,
19
+ type FlyoutItem,
26
20
  TermList,
27
21
  TitledIntro,
28
22
  cx,
@@ -32,10 +26,7 @@ import {
32
26
  useDomId,
33
27
  } from "./internal.js";
34
28
 
35
- export type FlyoutItem = Pick<
36
- LinkItem,
37
- "label" | "href" | "description" | "icon"
38
- >;
29
+ export type { FlyoutItem } from "./internal.js";
39
30
  /** A header link, or a labelled group that opens a flyout panel. */
40
31
  export type HeaderLink =
41
32
  | LinkAction
@@ -45,67 +36,6 @@ export type HeaderLink =
45
36
  layout?: "list" | "columns" | "full";
46
37
  });
47
38
 
48
- function Flyout({
49
- label,
50
- items,
51
- footer = [],
52
- layout = "list",
53
- onNavigate,
54
- }: {
55
- label: string;
56
- items: FlyoutItem[];
57
- footer?: LinkAction[];
58
- layout?: "list" | "columns" | "full";
59
- onNavigate: () => void;
60
- }) {
61
- const dataLayout = layout === "list" ? undefined : layout;
62
- return (
63
- <Popover className="m-flyout" data-layout={dataLayout}>
64
- <PopoverButton className="m-flyout-trigger">
65
- {label}
66
- <ChevronDown />
67
- </PopoverButton>
68
- {/* Anchored panels render in a portal, so headers inside clipped containers never cut them off. */}
69
- <PopoverPanel
70
- anchor="bottom"
71
- className="m-flyout-panel"
72
- data-layout={dataLayout}
73
- >
74
- <ul>
75
- {items.map((item, index) => (
76
- <li key={`${item.label}:${item.href}:${index}`}>
77
- <CloseButton as={KitLink} href={item.href} onClick={onNavigate}>
78
- {item.icon && (
79
- <span className="m-flyout-icon" aria-hidden="true">
80
- {item.icon}
81
- </span>
82
- )}
83
- <span className="m-flyout-text">
84
- <strong>{item.label}</strong>
85
- {item.description && <span>{item.description}</span>}
86
- </span>
87
- </CloseButton>
88
- </li>
89
- ))}
90
- </ul>
91
- {footer.length > 0 && (
92
- <div className="m-flyout-footer">
93
- {footer.map((link, index) => (
94
- <CloseButton
95
- as={KitLink}
96
- key={`${link.label}:${link.href}:${index}`}
97
- href={link.href}
98
- onClick={onNavigate}
99
- >
100
- {link.label}
101
- </CloseButton>
102
- ))}
103
- </div>
104
- )}
105
- </PopoverPanel>
106
- </Popover>
107
- );
108
- }
109
39
  export function MarketingHeader({
110
40
  brand,
111
41
  tagline,
@@ -444,6 +444,31 @@ function DescribedChoice({
444
444
  </div>
445
445
  );
446
446
  }
447
+ /**
448
+ * Keeps a choice in the described layout once it has had a description or
449
+ * error. Switching back to the plain label would remount the input and drop
450
+ * focus mid-interaction (for example a description that changes with the
451
+ * value). If the layout changes while the input has focus, focus follows it.
452
+ */
453
+ function useDescribedLayout(
454
+ active: boolean,
455
+ input: { current: HTMLInputElement | null },
456
+ ) {
457
+ const [described, setDescribed] = useState(false);
458
+ const refocus = useRef(false);
459
+ if (active && !described) {
460
+ refocus.current =
461
+ typeof document !== "undefined" &&
462
+ !!input.current &&
463
+ input.current === document.activeElement;
464
+ setDescribed(true);
465
+ }
466
+ useLayoutEffect(() => {
467
+ if (refocus.current) input.current?.focus();
468
+ refocus.current = false;
469
+ }, [described, input]);
470
+ return described;
471
+ }
447
472
  export function Checkbox({
448
473
  label,
449
474
  description,
@@ -458,23 +483,8 @@ export function Checkbox({
458
483
  error?: string;
459
484
  }) {
460
485
  const input = useRef<HTMLInputElement>(null);
461
- // Once an error has shown, keep the described layout: switching back to the
462
- // plain label would remount the input and drop focus while the person fixes
463
- // it. If the layout changes while the input has focus, focus follows it.
464
- const [described, setDescribed] = useState(false);
465
- const refocus = useRef(false);
466
- if (error && !described) {
467
- refocus.current =
468
- typeof document !== "undefined" &&
469
- !!input.current &&
470
- input.current === document.activeElement;
471
- setDescribed(true);
472
- }
473
- useLayoutEffect(() => {
474
- if (refocus.current) input.current?.focus();
475
- refocus.current = false;
476
- }, [described]);
477
- if (description || error || described)
486
+ const described = useDescribedLayout(!!(description || error), input);
487
+ if (described)
478
488
  return (
479
489
  <DescribedChoice
480
490
  component="Checkbox"
@@ -514,7 +524,9 @@ export function Switch({
514
524
  /** Secondary line under the label, announced as the description. */
515
525
  description?: ReactNode;
516
526
  }) {
517
- if (description)
527
+ const input = useRef<HTMLInputElement>(null);
528
+ const described = useDescribedLayout(!!description, input);
529
+ if (described)
518
530
  return (
519
531
  <DescribedChoice
520
532
  component="Switch"
@@ -526,6 +538,7 @@ export function Switch({
526
538
  <input
527
539
  {...props}
528
540
  {...attrs}
541
+ ref={input}
529
542
  type="checkbox"
530
543
  role="switch"
531
544
  className="m-switch"
@@ -535,7 +548,13 @@ export function Switch({
535
548
  );
536
549
  return (
537
550
  <label {...componentRoot("Switch")} className={cx("m-choice", className)}>
538
- <input {...props} type="checkbox" role="switch" className="m-switch" />
551
+ <input
552
+ {...props}
553
+ ref={input}
554
+ type="checkbox"
555
+ role="switch"
556
+ className="m-switch"
557
+ />
539
558
  {label}
540
559
  </label>
541
560
  );