@microbit/ui 0.1.0-alpha.14 → 0.1.0-alpha.16

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/Checkbox.tsx CHANGED
@@ -12,13 +12,33 @@ import { css, cx } from "styled-system/css";
12
12
  import { checkbox, CheckboxVariantProps } from "styled-system/recipes";
13
13
  import { SystemStyleObject } from "styled-system/types";
14
14
 
15
+ /** What a render-prop child is told about the checkbox. */
16
+ export interface CheckboxState {
17
+ isSelected: boolean;
18
+ isFocusVisible: boolean;
19
+ isDisabled: boolean;
20
+ }
21
+
15
22
  export interface CheckboxProps
16
23
  extends Omit<RACCheckboxProps, "className" | "children" | "style">,
17
24
  CheckboxVariantProps {
18
25
  /** Per-instance style overrides for the root, merged after the recipe. */
19
26
  css?: SystemStyleObject;
20
27
  className?: string;
21
- children?: ReactNode;
28
+ /**
29
+ * The label. A function receives the checkbox's state, for a label that
30
+ * changes with it.
31
+ */
32
+ children?: ReactNode | ((state: CheckboxState) => ReactNode);
33
+ /**
34
+ * Whether to draw the box. `false` is for a checkbox whose children draw
35
+ * the selected state themselves — a selectable tile, or an avatar that
36
+ * grows a tick. The label wrapper goes with it, so the children own the
37
+ * whole row, including the focus ring the box would otherwise carry.
38
+ *
39
+ * @default true
40
+ */
41
+ control?: boolean;
22
42
  }
23
43
 
24
44
  /**
@@ -31,6 +51,7 @@ export const Checkbox = ({
31
51
  css: cssProp,
32
52
  className,
33
53
  children,
54
+ control,
34
55
  ...rest
35
56
  }: CheckboxProps) => {
36
57
  const slots = checkbox({ size });
@@ -39,38 +60,47 @@ export const Checkbox = ({
39
60
  className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
40
61
  {...rest}
41
62
  >
42
- {({ isSelected, isFocusVisible, isDisabled }) => (
43
- <>
44
- <span
45
- className={slots.control}
46
- data-selected={isSelected || undefined}
47
- data-focus-visible={isFocusVisible || undefined}
48
- data-disabled={isDisabled || undefined}
49
- aria-hidden
50
- >
51
- {isSelected && (
52
- <svg viewBox="0 0 12 10" className={slots.icon} aria-hidden>
53
- <polyline
54
- points="1.5 6 4.5 9 10.5 1"
55
- fill="none"
56
- stroke="currentColor"
57
- strokeWidth="2"
58
- strokeLinecap="round"
59
- strokeLinejoin="round"
60
- />
61
- </svg>
62
- )}
63
- </span>
64
- {children != null && (
63
+ {({ isSelected, isFocusVisible, isDisabled }) => {
64
+ const content =
65
+ typeof children === "function"
66
+ ? children({ isSelected, isFocusVisible, isDisabled })
67
+ : children;
68
+ if (control === false) {
69
+ return content;
70
+ }
71
+ return (
72
+ <>
65
73
  <span
66
- className={slots.label}
74
+ className={slots.control}
75
+ data-selected={isSelected || undefined}
76
+ data-focus-visible={isFocusVisible || undefined}
67
77
  data-disabled={isDisabled || undefined}
78
+ aria-hidden
68
79
  >
69
- {children}
80
+ {isSelected && (
81
+ <svg viewBox="0 0 12 10" className={slots.icon} aria-hidden>
82
+ <polyline
83
+ points="1.5 6 4.5 9 10.5 1"
84
+ fill="none"
85
+ stroke="currentColor"
86
+ strokeWidth="2"
87
+ strokeLinecap="round"
88
+ strokeLinejoin="round"
89
+ />
90
+ </svg>
91
+ )}
70
92
  </span>
71
- )}
72
- </>
73
- )}
93
+ {content != null && (
94
+ <span
95
+ className={slots.label}
96
+ data-disabled={isDisabled || undefined}
97
+ >
98
+ {content}
99
+ </span>
100
+ )}
101
+ </>
102
+ );
103
+ }}
74
104
  </RACCheckbox>
75
105
  );
76
106
  };
@@ -0,0 +1,192 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import {
7
+ ForwardedRef,
8
+ forwardRef,
9
+ ReactNode,
10
+ useLayoutEffect,
11
+ useRef,
12
+ useState,
13
+ } from "react";
14
+ import {
15
+ Button as RACButton,
16
+ ComboBox as RACComboBox,
17
+ ComboBoxProps as RACComboBoxProps,
18
+ Input as RACInput,
19
+ Label as RACLabel,
20
+ ListBox as RACListBox,
21
+ Popover,
22
+ PopoverProps,
23
+ } from "react-aria-components";
24
+ import { RiArrowDownSLine } from "react-icons/ri";
25
+ import { css, cx } from "styled-system/css";
26
+ import { select, SelectVariantProps } from "styled-system/recipes";
27
+ import { SystemStyleObject } from "styled-system/types";
28
+ import { Icon } from "./Icon";
29
+ import { SelectSlotProvider } from "./Select";
30
+
31
+ export interface ComboBoxProps<T extends object>
32
+ extends Omit<RACComboBoxProps<T>, "className" | "children" | "style">,
33
+ SelectVariantProps {
34
+ /** Visible label. Use `aria-label` instead where the design has none. */
35
+ label?: ReactNode;
36
+ placeholder?: string;
37
+ /**
38
+ * Rendered inside the control, before the input — an icon for the current
39
+ * value, say. A ComboBox's control is a text input, so unlike a Select it
40
+ * cannot show anything but text for what is chosen; this is the way round
41
+ * that (react-select did it with a custom `SingleValue`).
42
+ */
43
+ startContent?: ReactNode;
44
+ /** `SelectOption`s. */
45
+ children: ReactNode;
46
+ /**
47
+ * Replaces the chevron; pass `null` for none, which is what a plain
48
+ * autocomplete wants (react-select's `dropdownIndicator: display none`).
49
+ */
50
+ indicator?: ReactNode | null;
51
+ /**
52
+ * Shown in place of the list when nothing matches (react-select's
53
+ * `noOptionsMessage`). Implies `allowsEmptyCollection`, since RAC otherwise
54
+ * closes the popover the moment the collection empties.
55
+ */
56
+ emptyState?: ReactNode;
57
+ /**
58
+ * Keep the dropdown shut until this prop is true. For gating on a minimum
59
+ * query length — react-aria has no `minLength`, and rendering an empty list
60
+ * still opens an empty card.
61
+ */
62
+ isPopoverHidden?: boolean;
63
+ placement?: PopoverProps["placement"];
64
+ /**
65
+ * Cap the dropdown's height (react-select's `maxMenuHeight`). A prop rather
66
+ * than a `contentCss` rule because RAC writes its own max-height inline
67
+ * while positioning, which beats any class.
68
+ */
69
+ maxHeight?: number;
70
+ /**
71
+ * Per-instance overrides for the control — the box around the input, its
72
+ * `startContent` and its indicator, which is what `Select`'s `css` styles
73
+ * too. Reach the input itself through the `select` recipe's `value` slot.
74
+ */
75
+ css?: SystemStyleObject;
76
+ /** Per-instance overrides for the dropdown card. */
77
+ contentCss?: SystemStyleObject;
78
+ className?: string;
79
+ }
80
+
81
+ /**
82
+ * ComboBox — a text input that filters a listbox, for choosing one of a known
83
+ * set where typing to narrow it down is the point. Use Select where the list
84
+ * is short enough to just pick from.
85
+ *
86
+ * Note the react-select difference this replaces: react-select filtered on
87
+ * `label` and kept the menu open on selection unless told otherwise, whereas
88
+ * react-aria filters on each item's `textValue` and closes on selection.
89
+ */
90
+ const ComboBoxInner = <T extends object>(
91
+ {
92
+ label,
93
+ placeholder,
94
+ startContent,
95
+ children,
96
+ indicator,
97
+ emptyState,
98
+ isPopoverHidden,
99
+ placement = "bottom start",
100
+ maxHeight,
101
+ css: cssProp,
102
+ contentCss,
103
+ className,
104
+ ...props
105
+ }: ComboBoxProps<T>,
106
+ ref: ForwardedRef<HTMLInputElement>,
107
+ ) => {
108
+ // As Select: forward whatever variant groups the merged recipe has.
109
+ const [variantProps, rest] = select.splitVariantProps(props);
110
+ const slots = select(variantProps);
111
+ // Anchor the card to the whole control, not to the bare input inside it —
112
+ // otherwise it hangs off the text baseline and is as narrow as the input.
113
+ const triggerRef = useRef<HTMLDivElement>(null);
114
+ // RAC's --trigger-width measures the input it anchors a ComboBox to, which
115
+ // is the control's content box — so a card sized from it is narrower than
116
+ // the field by the padding and border. Measure the control instead. State
117
+ // rather than reading the ref at render time: the popover is mounted from
118
+ // the first render, before the ref is set, and nothing would re-render it.
119
+ const [triggerWidth, setTriggerWidth] = useState<number>();
120
+ useLayoutEffect(() => {
121
+ const el = triggerRef.current;
122
+ if (!el) {
123
+ return;
124
+ }
125
+ const update = () => setTriggerWidth(el.offsetWidth);
126
+ update();
127
+ if (typeof ResizeObserver === "undefined") {
128
+ return;
129
+ }
130
+ const observer = new ResizeObserver(update);
131
+ observer.observe(el);
132
+ return () => observer.disconnect();
133
+ }, []);
134
+ return (
135
+ <SelectSlotProvider value={slots}>
136
+ <RACComboBox
137
+ allowsEmptyCollection={emptyState != null}
138
+ {...(rest as RACComboBoxProps<T>)}
139
+ className={cx(slots.root, className)}
140
+ >
141
+ {label != null && <RACLabel className={slots.label}>{label}</RACLabel>}
142
+ <div
143
+ ref={triggerRef}
144
+ className={cx(slots.trigger, cssProp ? css(cssProp) : undefined)}
145
+ >
146
+ {startContent}
147
+ <RACInput
148
+ ref={ref}
149
+ placeholder={placeholder}
150
+ className={slots.value}
151
+ />
152
+ {indicator !== null && (
153
+ <RACButton className={slots.indicator}>
154
+ {indicator ?? <Icon as={RiArrowDownSLine} />}
155
+ </RACButton>
156
+ )}
157
+ </div>
158
+ {!isPopoverHidden && (
159
+ <Popover
160
+ triggerRef={triggerRef}
161
+ placement={placement}
162
+ maxHeight={maxHeight}
163
+ style={triggerWidth ? { width: triggerWidth } : undefined}
164
+ className={cx(
165
+ slots.content,
166
+ contentCss ? css(contentCss) : undefined,
167
+ )}
168
+ >
169
+ <RACListBox
170
+ className={slots.list}
171
+ renderEmptyState={
172
+ emptyState
173
+ ? () => <div className={slots.empty}>{emptyState}</div>
174
+ : undefined
175
+ }
176
+ >
177
+ {children}
178
+ </RACListBox>
179
+ </Popover>
180
+ )}
181
+ </RACComboBox>
182
+ </SelectSlotProvider>
183
+ );
184
+ };
185
+
186
+ /**
187
+ * forwardRef with generics needs the cast (React's types cannot express it),
188
+ * so the ref lands on the input — call sites focus it for validation.
189
+ */
190
+ export const ComboBox = forwardRef(ComboBoxInner) as <T extends object>(
191
+ props: ComboBoxProps<T> & { ref?: ForwardedRef<HTMLInputElement> },
192
+ ) => ReturnType<typeof ComboBoxInner>;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { defineSlotRecipe } from "@pandacss/dev";
7
+
8
+ /**
9
+ * GridList slot recipe — a vertical list of selectable rows, each of which may
10
+ * hold its own interactive controls (which is what makes it a grid rather than
11
+ * a listbox: the roving tab index moves through rows, and the controls inside
12
+ * a row are reachable without leaving it).
13
+ *
14
+ * Chakra had no equivalent, so there is no Chakra look to match: the greys
15
+ * here are the family's neutral list styling, and an app with a strong
16
+ * selection colour restates them (classroom's roster does).
17
+ *
18
+ * Registered in the base preset (base-preset.ts), which also has the
19
+ * `staticCss` entry that keeps the runtime-prop variants generated.
20
+ */
21
+ export const gridList = defineSlotRecipe({
22
+ className: "grid-list",
23
+ slots: ["root", "item"],
24
+ base: {
25
+ root: {
26
+ // The list takes the roving tab index, so it is focusable itself and
27
+ // would otherwise draw the platform ring around the whole list.
28
+ outline: "none",
29
+ },
30
+ item: {
31
+ display: "flex",
32
+ alignItems: "center",
33
+ position: "relative",
34
+ // A row is interactive by definition — it selects, or it acts.
35
+ cursor: "pointer",
36
+ outline: "none",
37
+ transitionProperty: "background",
38
+ transitionDuration: "ultra-fast",
39
+ transitionTimingFunction: "ease-in",
40
+ _hover: { bg: "gray.50" },
41
+ // A row holding an open menu (or any other popover) keeps the hover
42
+ // grey, so the row an open menu belongs to stays visible. Hover state
43
+ // cannot do this on its own: a Popover lays a fixed full-viewport
44
+ // underlay over the page while open, which takes the pointer off the
45
+ // row — `:hover` and RAC's own `data-hovered` both drop the moment the
46
+ // menu appears. A trigger carries `aria-expanded` (useOverlayTrigger),
47
+ // so the row can see its own open overlay.
48
+ "&:has([aria-expanded=true])": { bg: "gray.50" },
49
+ "&[data-selected]": {
50
+ bg: "gray.100",
51
+ _hover: { bg: "gray.100" },
52
+ "&:has([aria-expanded=true])": { bg: "gray.100" },
53
+ },
54
+ "&[data-focus-visible]": { focusShadow: "outline" },
55
+ "&[data-disabled]": { opacity: 0.4, cursor: "not-allowed" },
56
+ },
57
+ },
58
+ });
@@ -0,0 +1,81 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { ReactNode } from "react";
7
+ import {
8
+ GridList as RACGridList,
9
+ GridListItem as RACGridListItem,
10
+ GridListItemProps as RACGridListItemProps,
11
+ GridListProps as RACGridListProps,
12
+ } from "react-aria-components";
13
+ import { css, cx } from "styled-system/css";
14
+ import { gridList } from "styled-system/recipes";
15
+ import { SystemStyleObject } from "styled-system/types";
16
+
17
+ export interface GridListProps<T extends object>
18
+ extends Omit<RACGridListProps<T>, "className" | "style" | "children"> {
19
+ /** `GridListItem`s, or a render function when `items` is given. */
20
+ children: RACGridListProps<T>["children"];
21
+ /** Per-instance style overrides for the list, merged after the recipe. */
22
+ css?: SystemStyleObject;
23
+ className?: string;
24
+ }
25
+
26
+ /**
27
+ * GridList — react-aria-components' <GridList>: a list of selectable rows,
28
+ * each of which may contain its own buttons and menus.
29
+ *
30
+ * Reach for it over a `ListBox` when the rows carry controls: a listbox option
31
+ * is a leaf, so a button inside one is unreachable by keyboard, where a grid
32
+ * row's contents are part of the grid's navigation.
33
+ */
34
+ export const GridList = <T extends object>({
35
+ css: cssProp,
36
+ className,
37
+ children,
38
+ ...rest
39
+ }: GridListProps<T>) => {
40
+ const slots = gridList();
41
+ return (
42
+ <RACGridList
43
+ {...rest}
44
+ className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
45
+ >
46
+ {children}
47
+ </RACGridList>
48
+ );
49
+ };
50
+
51
+ export interface GridListItemProps<T extends object = object>
52
+ extends Omit<RACGridListItemProps<T>, "className" | "style" | "children"> {
53
+ children?: ReactNode;
54
+ /** Per-instance style overrides for the row, merged after the recipe. */
55
+ css?: SystemStyleObject;
56
+ className?: string;
57
+ }
58
+
59
+ /**
60
+ * A row in a `GridList`. Its children are laid out by the row itself — the
61
+ * gridcell react-aria puts between them is `display: contents`.
62
+ *
63
+ * Give every row a `textValue`: react-aria derives typeahead text from string
64
+ * children only, and a row is usually a composition rather than a string.
65
+ */
66
+ export const GridListItem = <T extends object = object>({
67
+ css: cssProp,
68
+ className,
69
+ children,
70
+ ...rest
71
+ }: GridListItemProps<T>) => {
72
+ const slots = gridList();
73
+ return (
74
+ <RACGridListItem
75
+ {...rest}
76
+ className={cx(slots.item, cssProp ? css(cssProp) : undefined, className)}
77
+ >
78
+ {children}
79
+ </RACGridListItem>
80
+ );
81
+ };
package/src/Icon.tsx CHANGED
@@ -3,13 +3,27 @@
3
3
  *
4
4
  * SPDX-License-Identifier: MIT
5
5
  */
6
- import { IconType } from "react-icons/lib";
6
+ import { ComponentType, SVGProps } from "react";
7
7
  import { css, cx } from "styled-system/css";
8
8
  import { SystemStyleObject } from "styled-system/types";
9
9
 
10
+ /**
11
+ * Any component that renders an `<svg>` from svg props. Deliberately no
12
+ * narrower than the props `Icon` actually passes, so it accepts both
13
+ * react-icons' `IconType` and svgr components (`import X from "./x.svg?react"`,
14
+ * which the apps use for their custom-path icons — Chakra's `<Icon as={…}>`
15
+ * took either).
16
+ */
17
+ export type IconComponent = ComponentType<
18
+ Pick<
19
+ SVGProps<SVGSVGElement>,
20
+ "className" | "focusable" | "role" | "aria-label" | "aria-hidden"
21
+ >
22
+ >;
23
+
10
24
  export interface IconProps {
11
- /** The react-icons component to render. */
12
- as: IconType;
25
+ /** The icon component to render: a react-icons glyph or an svgr import. */
26
+ as: IconComponent;
13
27
  /** Panda style overrides (size via fontSize/boxSize, colour, etc.). */
14
28
  css?: SystemStyleObject;
15
29
  className?: string;
@@ -43,6 +57,12 @@ export const Icon = ({
43
57
  lineHeight: "1em",
44
58
  flexShrink: 0,
45
59
  fill: "currentColor",
60
+ // Chakra's Icon set this on the element itself, and an inline-block
61
+ // icon sits ~3px off without it. Panda's preflight happens to set it
62
+ // on every svg, which hid the omission in apps that had already
63
+ // flipped — classroom measured the difference at its kill-switch,
64
+ // where the preflight arrived and moved every icon back.
65
+ verticalAlign: "middle",
46
66
  ...cssProp,
47
67
  }),
48
68
  className,
package/src/Input.tsx CHANGED
@@ -23,14 +23,18 @@ export interface InputProps
23
23
  * labelled field with help/error text use TextField instead.
24
24
  */
25
25
  export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
26
- { size, css: cssProp, className, ...rest },
26
+ { css: cssProp, className, ...props },
27
27
  ref,
28
28
  ) {
29
+ // splitVariantProps, not a hand-picked `size`: an app preset can add variant
30
+ // groups to the recipe (classroom adds `variant`), and cherry-picking would
31
+ // silently drop them onto the DOM as unknown attributes instead.
32
+ const [variantProps, rest] = input.splitVariantProps(props);
29
33
  return (
30
34
  <input
31
35
  ref={ref}
32
36
  className={cx(
33
- input({ size }),
37
+ input(variantProps),
34
38
  cssProp ? css(cssProp) : undefined,
35
39
  className,
36
40
  )}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import { defineSlotRecipe } from "@pandacss/dev";
7
+
8
+ /**
9
+ * ListBox slot recipe — a standalone list of choosable options, single or
10
+ * multiple. Distinct from the `select` recipe's `list`/`option` slots, which
11
+ * style the same react-aria primitive inside a dropdown card: this one sits
12
+ * inline on the page, so it carries no surface of its own.
13
+ *
14
+ * An option is a leaf — if the rows need their own buttons or menus, they
15
+ * want `GridList` instead.
16
+ *
17
+ * Registered in the base preset (base-preset.ts), which also has the
18
+ * `staticCss` entry that keeps the runtime-prop variants generated.
19
+ */
20
+ export const listBox = defineSlotRecipe({
21
+ className: "list-box",
22
+ slots: ["root", "option"],
23
+ base: {
24
+ root: {
25
+ // The listbox holds the roving tab index, so it is focusable itself and
26
+ // would otherwise draw the platform ring around the whole list.
27
+ outline: "none",
28
+ },
29
+ option: {
30
+ display: "flex",
31
+ alignItems: "center",
32
+ cursor: "pointer",
33
+ outline: "none",
34
+ transitionProperty: "background",
35
+ transitionDuration: "ultra-fast",
36
+ transitionTimingFunction: "ease-in",
37
+ _hover: { bg: "gray.50" },
38
+ "&[data-selected]": { bg: "gray.100", _hover: { bg: "gray.100" } },
39
+ "&[data-focus-visible]": { focusShadow: "outline" },
40
+ "&[data-disabled]": { opacity: 0.4, cursor: "not-allowed" },
41
+ },
42
+ },
43
+ });
@@ -0,0 +1,88 @@
1
+ /**
2
+ * (c) 2026, Micro:bit Educational Foundation and contributors
3
+ *
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+ import {
7
+ ListBox as RACListBox,
8
+ ListBoxItem as RACListBoxItem,
9
+ ListBoxItemProps as RACListBoxItemProps,
10
+ ListBoxProps as RACListBoxProps,
11
+ } from "react-aria-components";
12
+ import { css, cx } from "styled-system/css";
13
+ import { listBox } from "styled-system/recipes";
14
+ import { SystemStyleObject } from "styled-system/types";
15
+
16
+ export interface ListBoxProps<T extends object>
17
+ extends Omit<RACListBoxProps<T>, "className" | "style" | "children"> {
18
+ /** `ListBoxOption`s, or a render function when `items` is given. */
19
+ children: RACListBoxProps<T>["children"];
20
+ /** Per-instance style overrides for the list, merged after the recipe. */
21
+ css?: SystemStyleObject;
22
+ className?: string;
23
+ }
24
+
25
+ /**
26
+ * ListBox — react-aria-components' <ListBox>, standing on the page rather
27
+ * than inside a dropdown (that is `Select`/`ComboBox`, which share their own
28
+ * recipe).
29
+ *
30
+ * Options are leaves: a button inside one is unreachable by keyboard, so rows
31
+ * carrying their own controls want `GridList`.
32
+ */
33
+ export const ListBox = <T extends object>({
34
+ css: cssProp,
35
+ className,
36
+ children,
37
+ ...rest
38
+ }: ListBoxProps<T>) => {
39
+ const slots = listBox();
40
+ return (
41
+ <RACListBox
42
+ {...rest}
43
+ className={cx(slots.root, cssProp ? css(cssProp) : undefined, className)}
44
+ >
45
+ {children}
46
+ </RACListBox>
47
+ );
48
+ };
49
+
50
+ export interface ListBoxOptionProps<T extends object = object>
51
+ extends Omit<RACListBoxItemProps<T>, "className" | "style" | "children"> {
52
+ /**
53
+ * The option's content. A function receives the option's state, for a row
54
+ * that draws its own selected marker rather than taking the recipe's
55
+ * background.
56
+ */
57
+ children?: RACListBoxItemProps<T>["children"];
58
+ /** Per-instance style overrides for the option, merged after the recipe. */
59
+ css?: SystemStyleObject;
60
+ className?: string;
61
+ }
62
+
63
+ /**
64
+ * An option in a `ListBox`.
65
+ *
66
+ * Give it a `textValue` where its children aren't a plain string: react-aria
67
+ * derives typeahead text from string children only.
68
+ */
69
+ export const ListBoxOption = <T extends object = object>({
70
+ css: cssProp,
71
+ className,
72
+ children,
73
+ ...rest
74
+ }: ListBoxOptionProps<T>) => {
75
+ const slots = listBox();
76
+ return (
77
+ <RACListBoxItem
78
+ {...rest}
79
+ className={cx(
80
+ slots.option,
81
+ cssProp ? css(cssProp) : undefined,
82
+ className,
83
+ )}
84
+ >
85
+ {children}
86
+ </RACListBoxItem>
87
+ );
88
+ };
@@ -35,7 +35,12 @@ export const menu = defineSlotRecipe({
35
35
  color: "inherit",
36
36
  minWidth: "3xs",
37
37
  py: "2",
38
- zIndex: "dropdown",
38
+ // `popover` (1500), not `dropdown` (1000): a RAC Popover always portals
39
+ // to the body, so a menu opened from inside a Modal (zIndex `modal`,
40
+ // 1400) escapes the modal's stacking context and would paint behind it.
41
+ // Chakra never hit this — its MenuList rendered inline unless explicitly
42
+ // portalled. Nothing else lives between 1400 and the toast/tooltip layer.
43
+ zIndex: "popover",
39
44
  borderRadius: "md",
40
45
  borderWidth: "1px",
41
46
  borderColor: "gray.200",