@astryxdesign/lab 0.1.4-canary.39ffd21 → 0.1.4-canary.3b5f42e

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.
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @file InfoTip.tsx
3
+ * @input Uses React, stylex, Tooltip + Icon from @astryxdesign/core, color/spacing/radius/duration/ease tokens
4
+ * @output Exports InfoTip component, InfoTipProps, InfoTipSize types
5
+ * @position Lab experiment (RFC facebook/astryx#3349); core implementation consumed by index.ts
6
+ *
7
+ * SYNC: When modified, update these files to stay in sync:
8
+ * - /packages/lab/src/InfoTip/InfoTip.doc.mjs (props table, features)
9
+ * - /packages/lab/src/InfoTip/InfoTip.test.tsx (tests for new/changed behavior)
10
+ * - /packages/lab/src/InfoTip/index.ts (exports if types change)
11
+ */
12
+ import { type ReactNode } from 'react';
13
+ import { type IconSize } from '@astryxdesign/core/Icon';
14
+ /**
15
+ * Size of the info icon. Maps 1:1 to Icon sizes
16
+ * (xsm: 12px, sm: 16px, md: 20px, lg: 24px).
17
+ */
18
+ export type InfoTipSize = IconSize;
19
+ export interface InfoTipProps {
20
+ /**
21
+ * Content to display in the tooltip.
22
+ * Typically short, non-interactive text. Mirrors Tooltip's `content` prop.
23
+ */
24
+ content: ReactNode;
25
+ /**
26
+ * Accessible name for the trigger button.
27
+ * @default 'More information'
28
+ */
29
+ label?: string;
30
+ /**
31
+ * Size of the info icon.
32
+ * @default 'sm'
33
+ */
34
+ size?: InfoTipSize;
35
+ }
36
+ /**
37
+ * An inline info-icon help affordance: a small "i" button that reveals a
38
+ * tooltip on hover and keyboard focus. Use it next to labels, values, and
39
+ * metrics for permission notes, metric definitions, and field help.
40
+ *
41
+ * The value over hand-composing Icon inside Tooltip is the pre-wired
42
+ * accessible trigger: a real button with an aria-label, Tab-reachable,
43
+ * tooltip on hover AND focus, and Escape dismissal.
44
+ *
45
+ * Composed entirely from core primitives (Tooltip + Icon); the info icon
46
+ * resolves from the global icon registry, so themes can override it.
47
+ *
48
+ * @example
49
+ * ```
50
+ * <InfoTip content="Editors can change this field; viewers cannot." />
51
+ * <InfoTip content="30-day rolling average." label="About this metric" />
52
+ * ```
53
+ */
54
+ export declare function InfoTip({ content, label, size, }: InfoTipProps): ReactNode;
55
+ export declare namespace InfoTip {
56
+ var displayName: string;
57
+ }
58
+ //# sourceMappingURL=InfoTip.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InfoTip.d.ts","sourceRoot":"","sources":["../../src/InfoTip/InfoTip.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH,OAAO,EAAgC,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAGpE,OAAO,EAAO,KAAK,QAAQ,EAAC,MAAM,yBAAyB,CAAC;AAU5D;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC;AAEnC,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,OAAO,EAAE,SAAS,CAAC;IACnB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,IAAI,CAAC,EAAE,WAAW,CAAC;CACpB;AAmCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,OAAO,CAAC,EACtB,OAAO,EACP,KAA0B,EAC1B,IAAW,GACZ,EAAE,YAAY,GAAG,SAAS,CA4C1B;yBAhDe,OAAO"}
@@ -0,0 +1,91 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file InfoTip.tsx
7
+ * @input Uses React, stylex, Tooltip + Icon from @astryxdesign/core, color/spacing/radius/duration/ease tokens
8
+ * @output Exports InfoTip component, InfoTipProps, InfoTipSize types
9
+ * @position Lab experiment (RFC facebook/astryx#3349); core implementation consumed by index.ts
10
+ *
11
+ * SYNC: When modified, update these files to stay in sync:
12
+ * - /packages/lab/src/InfoTip/InfoTip.doc.mjs (props table, features)
13
+ * - /packages/lab/src/InfoTip/InfoTip.test.tsx (tests for new/changed behavior)
14
+ * - /packages/lab/src/InfoTip/index.ts (exports if types change)
15
+ */
16
+ import { useCallback, useRef, useState } from 'react';
17
+ import * as stylex from '@stylexjs/stylex';
18
+ import { Icon } from '@astryxdesign/core/Icon';
19
+ import { Tooltip } from '@astryxdesign/core/Tooltip';
20
+ import '@astryxdesign/core/theme/tokens.stylex';
21
+ import { colorVars, durationVars, easeVars, radiusVars, spacingVars } from '@astryxdesign/core/theme/tokens.stylex';
22
+
23
+ /**
24
+ * Size of the info icon. Maps 1:1 to Icon sizes
25
+ * (xsm: 12px, sm: 16px, md: 20px, lg: 24px).
26
+ */
27
+ import { jsx as _jsx } from "react/jsx-runtime";
28
+ /**
29
+ * An inline info-icon help affordance: a small "i" button that reveals a
30
+ * tooltip on hover and keyboard focus. Use it next to labels, values, and
31
+ * metrics for permission notes, metric definitions, and field help.
32
+ *
33
+ * The value over hand-composing Icon inside Tooltip is the pre-wired
34
+ * accessible trigger: a real button with an aria-label, Tab-reachable,
35
+ * tooltip on hover AND focus, and Escape dismissal.
36
+ *
37
+ * Composed entirely from core primitives (Tooltip + Icon); the info icon
38
+ * resolves from the global icon registry, so themes can override it.
39
+ *
40
+ * @example
41
+ * ```
42
+ * <InfoTip content="Editors can change this field; viewers cannot." />
43
+ * <InfoTip content="30-day rolling average." label="About this metric" />
44
+ * ```
45
+ */
46
+ export function InfoTip({
47
+ content,
48
+ label = 'More information',
49
+ size = 'sm'
50
+ }) {
51
+ // Escape dismissal: `true` force-hides the tooltip (Tooltip isOpen={false});
52
+ // `false` returns control to Tooltip's own hover/focus triggers (isOpen
53
+ // undefined). Reset when the pointer or focus leaves the trigger so the
54
+ // tooltip can re-open on the next hover/focus.
55
+ const [isDismissed, setIsDismissed] = useState(false);
56
+ const isOpenRef = useRef(false);
57
+ const handleOpenChange = useCallback(isOpen => {
58
+ isOpenRef.current = isOpen;
59
+ }, []);
60
+ const handleKeyDown = useCallback(event => {
61
+ if (event.key === 'Escape' && isOpenRef.current) {
62
+ // Only swallow Escape when it actually dismissed the tooltip, so an
63
+ // enclosing dialog still closes on the next press.
64
+ event.stopPropagation();
65
+ setIsDismissed(true);
66
+ }
67
+ }, []);
68
+ const handleReset = useCallback(() => {
69
+ setIsDismissed(false);
70
+ }, []);
71
+ return /*#__PURE__*/_jsx(Tooltip, {
72
+ content: content,
73
+ isOpen: isDismissed ? false : undefined,
74
+ onOpenChange: handleOpenChange,
75
+ children: /*#__PURE__*/_jsx("button", {
76
+ type: "button",
77
+ "aria-label": label,
78
+ onKeyDown: handleKeyDown,
79
+ onBlur: handleReset,
80
+ onMouseLeave: handleReset,
81
+ ...{
82
+ className: "x3nfvp2 x6s0dn4 xl56j7k xxymvpz x12nba7r x1ghz6dp xng3xce xjbqb8w xjspbzw x1ypdohk xv9yike xkxie9p x17nn4n9 x1wfwxd8 x7s97pk xt3l3uh xuedmi6 xlr8y92"
83
+ },
84
+ children: /*#__PURE__*/_jsx(Icon, {
85
+ icon: "info",
86
+ size: size
87
+ })
88
+ })
89
+ });
90
+ }
91
+ InfoTip.displayName = 'InfoTip';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @file InfoTip component barrel export
3
+ */
4
+ export { InfoTip } from './InfoTip';
5
+ export type { InfoTipProps, InfoTipSize } from './InfoTip';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/InfoTip/index.ts"],"names":[],"mappings":"AAIA;;GAEG;AAEH,OAAO,EAAC,OAAO,EAAC,MAAM,WAAW,CAAC;AAClC,YAAY,EAAC,YAAY,EAAE,WAAW,EAAC,MAAM,WAAW,CAAC"}
@@ -0,0 +1,8 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file InfoTip component barrel export
7
+ */
8
+ export { InfoTip } from "./InfoTip.js";
package/dist/index.d.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  export { CodeBlock, type CodeBlockProps } from './CodeBlock';
13
13
  export { CodeEditor, type CodeEditorProps } from './CodeEditor';
14
14
  export { tokenize, tokenizeAsync, SYNC_TOKENIZE_THRESHOLD, type SyntaxToken, } from '@astryxdesign/core/CodeBlock';
15
+ export { InfoTip, type InfoTipProps, type InfoTipSize } from './InfoTip';
15
16
  export { ChatReasoning, type ChatReasoningProps, } from './ChatReasoning/ChatReasoning';
16
17
  export * from './Schedule';
17
18
  export { SVGIcon, type SVGIconProps, type SVGIconVariation, type SVGIconSize, type SVGIconColor, type SVGIconDef, type IconShape, type IconShapeRole, iconVars, variations, opticalSize, xIcon, checkIcon, bellIcon, homeIcon, settingsIcon, calendarIcon, menuIcon, heartIcon, eyeIcon, starIcon, folderIcon, shieldIcon, searchIcon, mailIcon, lockIcon, starterIcons, } from './SVGIcon';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -17,6 +17,9 @@ export { CodeBlock } from "./CodeBlock/index.js";
17
17
  export { CodeEditor } from "./CodeEditor/index.js";
18
18
  export { tokenize, tokenizeAsync, SYNC_TOKENIZE_THRESHOLD } from '@astryxdesign/core/CodeBlock';
19
19
 
20
+ // InfoTip — accessible info-icon help affordance (RFC facebook/astryx#3349)
21
+ export { InfoTip } from "./InfoTip/index.js";
22
+
20
23
  // Chat — experimental reasoning display
21
24
  export { ChatReasoning } from "./ChatReasoning/ChatReasoning.js";
22
25
 
package/dist/lab.css CHANGED
@@ -52,6 +52,7 @@
52
52
  .x1ghz6dp:not(#\#){margin:0}
53
53
  .x10h29id:not(#\#){margin:var(--spacing-0-5)}
54
54
  .x1717udv:not(#\#){padding:0}
55
+ .x12nba7r:not(#\#){padding:var(--spacing-0-5)}
55
56
  .x103w4nt:not(#\#):not(#\#){border-color:color-mix(in srgb,var(--color-border-blue) 48%,var(--color-border))}
56
57
  .x14n1b0s:not(#\#):not(#\#){border-color:color-mix(in srgb,var(--color-border-cyan) 48%,var(--color-border))}
57
58
  .x1uk4s2l:not(#\#):not(#\#){border-color:color-mix(in srgb,var(--color-border-green) 48%,var(--color-border))}
@@ -79,6 +80,7 @@
79
80
  .xh6dtrn:not(#\#):not(#\#){border-radius:var(--radius-element)}
80
81
  .xjspbzw:not(#\#):not(#\#){border-radius:var(--radius-full)}
81
82
  .xx3sua9:not(#\#):not(#\#){border-radius:var(--radius-inner)}
83
+ .xng3xce:not(#\#):not(#\#){border-style:none}
82
84
  .x1y0btm7:not(#\#):not(#\#){border-style:solid}
83
85
  .xc342km:not(#\#):not(#\#){border-width:0}
84
86
  .x1litavf:not(#\#):not(#\#){border-width:var(--border-width)}
@@ -326,11 +328,14 @@
326
328
  .x19jd1h0:not(#\#):not(#\#):not(#\#){transform:rotate(180deg)}
327
329
  .x1cb1t30:not(#\#):not(#\#):not(#\#){transform:translateY(-50%)}
328
330
  .xkvfbh3:not(#\#):not(#\#):not(#\#){transition-duration:var(--duration-fast-min)}
331
+ .xuedmi6:not(#\#):not(#\#):not(#\#){transition-duration:var(--duration-fast)}
329
332
  .x80gvsz:not(#\#):not(#\#):not(#\#){transition-duration:var(--duration-medium)}
330
333
  .x15406qy:not(#\#):not(#\#):not(#\#){transition-property:background-color}
334
+ .xt3l3uh:not(#\#):not(#\#):not(#\#){transition-property:color}
331
335
  .xxnu56j:not(#\#):not(#\#):not(#\#){transition-property:stroke-dashoffset}
332
336
  .xlr8y92:not(#\#):not(#\#):not(#\#){transition-timing-function:var(--ease-standard)}
333
337
  .x87ps6o:not(#\#):not(#\#):not(#\#){user-select:none}
338
+ .xxymvpz:not(#\#):not(#\#):not(#\#){vertical-align:middle}
334
339
  .xlshs6z:not(#\#):not(#\#):not(#\#){visibility:hidden}
335
340
  .xuxw1ft:not(#\#):not(#\#):not(#\#){white-space:nowrap}
336
341
  .x1sdyfia:not(#\#):not(#\#):not(#\#){white-space:pre}
@@ -344,6 +349,7 @@
344
349
  @media (prefers-reduced-motion: reduce){.xnh0sag.xnh0sag:not(#\#):not(#\#):not(#\#){animation-duration:3s}}
345
350
  @media (prefers-reduced-motion: reduce){.xy02sl2.xy02sl2:not(#\#):not(#\#):not(#\#){animation-duration:4s}}
346
351
  @media (hover: hover){.xe9uy6x.xe9uy6x:hover:not(#\#):not(#\#):not(#\#){background-color:var(--color-overlay-hover)}}
352
+ @media (hover: hover){.xkxie9p.xkxie9p:hover:not(#\#):not(#\#):not(#\#){color:var(--color-icon-primary)}}
347
353
  .xw8gpjh:not(#\#):not(#\#):not(#\#):not(#\#){border-bottom-color:var(--color-border)}
348
354
  .x1q0q8m5:not(#\#):not(#\#):not(#\#):not(#\#){border-bottom-style:solid}
349
355
  .x1qhh985:not(#\#):not(#\#):not(#\#):not(#\#){border-bottom-width:0}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/lab",
3
- "version": "0.1.4-canary.39ffd21",
3
+ "version": "0.1.4-canary.3b5f42e",
4
4
  "description": "Experimental Astryx components — published to npm only under the @canary dist-tag for early testing; never released as a stable (latest) version.",
5
5
  "author": "Meta Open Source",
6
6
  "license": "MIT",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@stylexjs/stylex": ">=0.10.0",
48
- "@astryxdesign/core": "0.1.4-canary.39ffd21",
48
+ "@astryxdesign/core": "0.1.4-canary.3b5f42e",
49
49
  "react": ">=19.0.0",
50
50
  "react-dom": ">=19.0.0"
51
51
  },
@@ -0,0 +1,97 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('../docs-types').ComponentDoc} */
4
+
5
+ export const docs = {
6
+ name: 'InfoTip',
7
+ displayName: 'Info Tip',
8
+ category: 'Feedback & Status',
9
+ keywords: ["info","tooltip","help","hint","affordance","icon","label","field","metric","definition"],
10
+ props: [
11
+ {
12
+ name: 'content',
13
+ type: 'ReactNode',
14
+ description: 'Content to display in the tooltip. Typically short, non-interactive text. Mirrors Tooltip’s content prop.',
15
+ required: true,
16
+ },
17
+ {
18
+ name: 'label',
19
+ type: 'string',
20
+ description: 'Accessible name for the trigger button.',
21
+ default: "'More information'",
22
+ },
23
+ {
24
+ name: 'size',
25
+ type: "'xsm' | 'sm' | 'md' | 'lg'",
26
+ description: 'Size of the info icon (12px, 16px, 20px, 24px). Maps 1:1 to Icon sizes.',
27
+ default: "'sm'",
28
+ },
29
+ ],
30
+ usage: {
31
+ description:
32
+ 'An inline info-icon help affordance: a small "i" button that reveals a tooltip on hover and keyboard focus. Use it next to labels, values, and metrics for permission notes, metric definitions, and field help. Its value over hand-composing Icon inside Tooltip is the pre-wired accessible trigger: a real button with an aria-label, Tab-reachable, tooltip on hover AND focus, and Escape dismissal.',
33
+ bestPractices: [
34
+ { guidance: true, description: 'Keep tooltip content concise, plain, and non-interactive; use Popover or HoverCard for links and buttons.' },
35
+ { guidance: true, description: 'Override label when "More information" is too generic, e.g. "About this metric".' },
36
+ { guidance: false, description: 'Hand-compose Icon inside Tooltip for info affordances; the bare Icon is aria-hidden and unfocusable, so keyboard and screen-reader users never see it.' },
37
+ { guidance: false, description: 'Use InfoTip for essential information users must see to complete a task; put that text inline instead.' },
38
+ ],
39
+ },
40
+ };
41
+
42
+ /** @type {import('../docs-types').ComponentDoc} */
43
+ export const docsZh = {
44
+ name: 'InfoTip',
45
+ displayName: 'Info Tip',
46
+ props: [
47
+ {
48
+ name: 'content',
49
+ type: 'ReactNode',
50
+ description: '工具提示中显示的内容。通常为简短的非交互文本。与 Tooltip 的 content 属性一致。',
51
+ required: true,
52
+ },
53
+ {
54
+ name: 'label',
55
+ type: 'string',
56
+ description: '触发按钮的无障碍名称。',
57
+ default: "'More information'",
58
+ },
59
+ {
60
+ name: 'size',
61
+ type: "'xsm' | 'sm' | 'md' | 'lg'",
62
+ description: '信息图标的尺寸(12px、16px、20px、24px)。与 Icon 尺寸一一对应。',
63
+ default: "'sm'",
64
+ },
65
+ ],
66
+ usage: {
67
+ description:
68
+ 'An inline info-icon help affordance: a small "i" button that reveals a tooltip on hover and keyboard focus. Use it next to labels, values, and metrics for permission notes, metric definitions, and field help.',
69
+ bestPractices: [
70
+ { guidance: true, description: 'Keep tooltip content concise, plain, and non-interactive; use Popover or HoverCard for links and buttons.' },
71
+ { guidance: true, description: 'Override label when "More information" is too generic, e.g. "About this metric".' },
72
+ { guidance: false, description: 'Hand-compose Icon inside Tooltip for info affordances; the bare Icon is aria-hidden and unfocusable, so keyboard and screen-reader users never see it.' },
73
+ { guidance: false, description: 'Use InfoTip for essential information users must see to complete a task; put that text inline instead.' },
74
+ ],
75
+ },
76
+ };
77
+
78
+ /** @type {import('../docs-types').TranslationDoc} */
79
+ export const docsDense = {
80
+ description:
81
+ 'Inline info-icon help affordance: accessible "i" button trigger pre-wired into a Tooltip.',
82
+ usage: {
83
+ description:
84
+ 'A small "i" button that reveals a tooltip on hover and keyboard focus. Use next to labels, values, and metrics for permission notes, metric definitions, and field help. Pre-wired accessible trigger: real button, aria-label, Tab-reachable, tooltip on hover AND focus, Escape dismissal.',
85
+ bestPractices: [
86
+ { guidance: true, description: 'Keep tooltip content concise, plain, and non-interactive; use Popover or HoverCard for links and buttons.' },
87
+ { guidance: true, description: 'Override label when "More information" is too generic, e.g. "About this metric".' },
88
+ { guidance: false, description: 'Hand-compose Icon inside Tooltip for info affordances; the bare Icon is aria-hidden and unfocusable.' },
89
+ { guidance: false, description: 'Use InfoTip for essential information users must see to complete a task; put that text inline instead.' },
90
+ ],
91
+ },
92
+ propDescriptions: {
93
+ content: 'Tooltip content. Short, non-interactive text. Mirrors Tooltip content prop.',
94
+ label: "Accessible name for the trigger button. Default 'More information'.",
95
+ size: 'Info icon size (12/16/20/24px). Maps 1:1 to Icon sizes.',
96
+ },
97
+ };
@@ -0,0 +1,169 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file InfoTip.test.tsx
5
+ * @input Uses vitest, @testing-library/react, @testing-library/user-event, InfoTip component
6
+ * @output Unit tests for InfoTip trigger accessibility and tooltip behavior
7
+ * @position Testing; validates InfoTip.tsx implementation
8
+ *
9
+ * SYNC: When InfoTip.tsx changes, update tests to match new behavior
10
+ */
11
+
12
+ import {describe, it, expect, vi, beforeAll, afterAll} from 'vitest';
13
+ import {render, screen, fireEvent, waitFor, act} from '@testing-library/react';
14
+ import userEvent from '@testing-library/user-event';
15
+ import {InfoTip} from './InfoTip';
16
+
17
+ // Store original matches to restore later
18
+ const originalMatches = HTMLElement.prototype.matches;
19
+
20
+ // Track popover open state per element
21
+ const popoverOpenState = new WeakMap<HTMLElement, boolean>();
22
+
23
+ // Mock Popover API for jsdom, and :focus-visible for keyboard-focus tests
24
+ // (same harness as core Tooltip.test.tsx, plus :focus-visible)
25
+ beforeAll(() => {
26
+ HTMLElement.prototype.showPopover = vi.fn(function (this: HTMLElement) {
27
+ popoverOpenState.set(this, true);
28
+ });
29
+ HTMLElement.prototype.hidePopover = vi.fn(function (this: HTMLElement) {
30
+ popoverOpenState.set(this, false);
31
+ });
32
+
33
+ // Intercept :popover-open and :focus-visible, delegate everything else
34
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
+ (HTMLElement.prototype as any).matches = function (
36
+ selector: string,
37
+ ): boolean {
38
+ if (selector === ':popover-open') {
39
+ return popoverOpenState.get(this) ?? false;
40
+ }
41
+ if (selector === ':focus-visible') {
42
+ // Treat any focused element as keyboard-focused in tests
43
+ return this === document.activeElement;
44
+ }
45
+ return originalMatches.call(this, selector);
46
+ };
47
+ });
48
+
49
+ afterAll(() => {
50
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
+ (HTMLElement.prototype as any).matches = originalMatches;
52
+ });
53
+
54
+ /** Focus the trigger the way a keyboard user would land on it. */
55
+ function focusTrigger(trigger: HTMLElement) {
56
+ act(() => {
57
+ trigger.focus();
58
+ });
59
+ fireEvent.focusIn(trigger);
60
+ }
61
+
62
+ describe('InfoTip', () => {
63
+ it('renders a button trigger with the default accessible name', () => {
64
+ render(<InfoTip content="Helpful context" />);
65
+ expect(
66
+ screen.getByRole('button', {name: 'More information'}),
67
+ ).toBeInTheDocument();
68
+ });
69
+
70
+ it('uses a custom label as the accessible name', () => {
71
+ render(
72
+ <InfoTip content="30-day rolling average." label="About this metric" />,
73
+ );
74
+ const trigger = screen.getByRole('button', {name: 'About this metric'});
75
+ expect(trigger).toHaveAttribute('aria-label', 'About this metric');
76
+ });
77
+
78
+ it('links the trigger to the tooltip layer via aria-describedby', () => {
79
+ render(<InfoTip content="Helpful context" />);
80
+ const layer = screen.getByRole('tooltip', {hidden: true});
81
+ expect(layer).toHaveTextContent('Helpful context');
82
+ const trigger = screen.getByRole('button', {name: 'More information'});
83
+ expect(trigger.getAttribute('aria-describedby')).toBe(layer.id);
84
+ });
85
+
86
+ it('is reachable with Tab', async () => {
87
+ const user = userEvent.setup();
88
+ render(<InfoTip content="Helpful context" />);
89
+ const trigger = screen.getByRole('button', {name: 'More information'});
90
+ await user.tab();
91
+ expect(trigger).toHaveFocus();
92
+ });
93
+
94
+ it('shows the tooltip on hover', async () => {
95
+ vi.mocked(HTMLElement.prototype.showPopover).mockClear();
96
+ render(<InfoTip content="Helpful context" />);
97
+ const trigger = screen.getByRole('button', {name: 'More information'});
98
+
99
+ fireEvent.mouseEnter(trigger);
100
+
101
+ await waitFor(() => {
102
+ expect(HTMLElement.prototype.showPopover).toHaveBeenCalled();
103
+ });
104
+ });
105
+
106
+ it('shows the tooltip on keyboard focus', async () => {
107
+ vi.mocked(HTMLElement.prototype.showPopover).mockClear();
108
+ render(<InfoTip content="Helpful context" />);
109
+ const trigger = screen.getByRole('button', {name: 'More information'});
110
+
111
+ focusTrigger(trigger);
112
+
113
+ await waitFor(() => {
114
+ expect(HTMLElement.prototype.showPopover).toHaveBeenCalled();
115
+ });
116
+ const layer = screen.getByRole('tooltip', {hidden: true});
117
+ expect(popoverOpenState.get(layer)).toBe(true);
118
+ });
119
+
120
+ it('closes the tooltip on Escape', async () => {
121
+ render(<InfoTip content="Helpful context" />);
122
+ const trigger = screen.getByRole('button', {name: 'More information'});
123
+ const layer = screen.getByRole('tooltip', {hidden: true});
124
+
125
+ focusTrigger(trigger);
126
+ await waitFor(() => {
127
+ expect(popoverOpenState.get(layer)).toBe(true);
128
+ });
129
+
130
+ fireEvent.keyDown(trigger, {key: 'Escape'});
131
+
132
+ await waitFor(() => {
133
+ expect(popoverOpenState.get(layer)).toBe(false);
134
+ });
135
+ });
136
+
137
+ it('re-opens after Escape once the trigger is left and re-hovered', async () => {
138
+ render(<InfoTip content="Helpful context" />);
139
+ const trigger = screen.getByRole('button', {name: 'More information'});
140
+ const layer = screen.getByRole('tooltip', {hidden: true});
141
+
142
+ focusTrigger(trigger);
143
+ await waitFor(() => {
144
+ expect(popoverOpenState.get(layer)).toBe(true);
145
+ });
146
+
147
+ fireEvent.keyDown(trigger, {key: 'Escape'});
148
+ await waitFor(() => {
149
+ expect(popoverOpenState.get(layer)).toBe(false);
150
+ });
151
+
152
+ // Leave (resets dismissal), then re-enter
153
+ act(() => {
154
+ trigger.blur();
155
+ });
156
+ fireEvent.focusOut(trigger);
157
+ fireEvent.mouseLeave(trigger);
158
+
159
+ fireEvent.mouseEnter(trigger);
160
+ await waitFor(() => {
161
+ expect(popoverOpenState.get(layer)).toBe(true);
162
+ });
163
+ });
164
+
165
+ it('renders ReactNode tooltip content', () => {
166
+ render(<InfoTip content={<span data-testid="rich-content">Rich</span>} />);
167
+ expect(screen.getByTestId('rich-content')).toBeInTheDocument();
168
+ });
169
+ });
@@ -0,0 +1,155 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file InfoTip.tsx
7
+ * @input Uses React, stylex, Tooltip + Icon from @astryxdesign/core, color/spacing/radius/duration/ease tokens
8
+ * @output Exports InfoTip component, InfoTipProps, InfoTipSize types
9
+ * @position Lab experiment (RFC facebook/astryx#3349); core implementation consumed by index.ts
10
+ *
11
+ * SYNC: When modified, update these files to stay in sync:
12
+ * - /packages/lab/src/InfoTip/InfoTip.doc.mjs (props table, features)
13
+ * - /packages/lab/src/InfoTip/InfoTip.test.tsx (tests for new/changed behavior)
14
+ * - /packages/lab/src/InfoTip/index.ts (exports if types change)
15
+ */
16
+
17
+ import {useCallback, useRef, useState, type ReactNode} from 'react';
18
+ import * as stylex from '@stylexjs/stylex';
19
+
20
+ import {Icon, type IconSize} from '@astryxdesign/core/Icon';
21
+ import {Tooltip} from '@astryxdesign/core/Tooltip';
22
+ import {
23
+ colorVars,
24
+ durationVars,
25
+ easeVars,
26
+ radiusVars,
27
+ spacingVars,
28
+ } from '@astryxdesign/core/theme/tokens.stylex';
29
+
30
+ /**
31
+ * Size of the info icon. Maps 1:1 to Icon sizes
32
+ * (xsm: 12px, sm: 16px, md: 20px, lg: 24px).
33
+ */
34
+ export type InfoTipSize = IconSize;
35
+
36
+ export interface InfoTipProps {
37
+ /**
38
+ * Content to display in the tooltip.
39
+ * Typically short, non-interactive text. Mirrors Tooltip's `content` prop.
40
+ */
41
+ content: ReactNode;
42
+ /**
43
+ * Accessible name for the trigger button.
44
+ * @default 'More information'
45
+ */
46
+ label?: string;
47
+ /**
48
+ * Size of the info icon.
49
+ * @default 'sm'
50
+ */
51
+ size?: InfoTipSize;
52
+ }
53
+
54
+ const styles = stylex.create({
55
+ trigger: {
56
+ display: 'inline-flex',
57
+ alignItems: 'center',
58
+ justifyContent: 'center',
59
+ verticalAlign: 'middle',
60
+ padding: spacingVars['--spacing-0-5'],
61
+ margin: 0,
62
+ borderStyle: 'none',
63
+ backgroundColor: 'transparent',
64
+ borderRadius: radiusVars['--radius-full'],
65
+ cursor: 'pointer',
66
+ color: {
67
+ default: colorVars['--color-icon-secondary'],
68
+ ':hover': {
69
+ default: null,
70
+ '@media (hover: hover)': colorVars['--color-icon-primary'],
71
+ },
72
+ },
73
+ outline: {
74
+ default: null,
75
+ ':focus-visible': `2px solid ${colorVars['--color-accent']}`,
76
+ },
77
+ outlineOffset: {
78
+ default: '0',
79
+ ':focus-visible': '2px',
80
+ },
81
+ transitionProperty: 'color',
82
+ transitionDuration: durationVars['--duration-fast'],
83
+ transitionTimingFunction: easeVars['--ease-standard'],
84
+ },
85
+ });
86
+
87
+ /**
88
+ * An inline info-icon help affordance: a small "i" button that reveals a
89
+ * tooltip on hover and keyboard focus. Use it next to labels, values, and
90
+ * metrics for permission notes, metric definitions, and field help.
91
+ *
92
+ * The value over hand-composing Icon inside Tooltip is the pre-wired
93
+ * accessible trigger: a real button with an aria-label, Tab-reachable,
94
+ * tooltip on hover AND focus, and Escape dismissal.
95
+ *
96
+ * Composed entirely from core primitives (Tooltip + Icon); the info icon
97
+ * resolves from the global icon registry, so themes can override it.
98
+ *
99
+ * @example
100
+ * ```
101
+ * <InfoTip content="Editors can change this field; viewers cannot." />
102
+ * <InfoTip content="30-day rolling average." label="About this metric" />
103
+ * ```
104
+ */
105
+ export function InfoTip({
106
+ content,
107
+ label = 'More information',
108
+ size = 'sm',
109
+ }: InfoTipProps): ReactNode {
110
+ // Escape dismissal: `true` force-hides the tooltip (Tooltip isOpen={false});
111
+ // `false` returns control to Tooltip's own hover/focus triggers (isOpen
112
+ // undefined). Reset when the pointer or focus leaves the trigger so the
113
+ // tooltip can re-open on the next hover/focus.
114
+ const [isDismissed, setIsDismissed] = useState(false);
115
+ const isOpenRef = useRef(false);
116
+
117
+ const handleOpenChange = useCallback((isOpen: boolean) => {
118
+ isOpenRef.current = isOpen;
119
+ }, []);
120
+
121
+ const handleKeyDown = useCallback(
122
+ (event: React.KeyboardEvent<HTMLButtonElement>) => {
123
+ if (event.key === 'Escape' && isOpenRef.current) {
124
+ // Only swallow Escape when it actually dismissed the tooltip, so an
125
+ // enclosing dialog still closes on the next press.
126
+ event.stopPropagation();
127
+ setIsDismissed(true);
128
+ }
129
+ },
130
+ [],
131
+ );
132
+
133
+ const handleReset = useCallback(() => {
134
+ setIsDismissed(false);
135
+ }, []);
136
+
137
+ return (
138
+ <Tooltip
139
+ content={content}
140
+ isOpen={isDismissed ? false : undefined}
141
+ onOpenChange={handleOpenChange}>
142
+ <button
143
+ type="button"
144
+ aria-label={label}
145
+ onKeyDown={handleKeyDown}
146
+ onBlur={handleReset}
147
+ onMouseLeave={handleReset}
148
+ {...stylex.props(styles.trigger)}>
149
+ <Icon icon="info" size={size} />
150
+ </button>
151
+ </Tooltip>
152
+ );
153
+ }
154
+
155
+ InfoTip.displayName = 'InfoTip';
@@ -0,0 +1,10 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file InfoTip component barrel export
7
+ */
8
+
9
+ export {InfoTip} from './InfoTip';
10
+ export type {InfoTipProps, InfoTipSize} from './InfoTip';
package/src/index.ts CHANGED
@@ -22,6 +22,9 @@ export {
22
22
  type SyntaxToken,
23
23
  } from '@astryxdesign/core/CodeBlock';
24
24
 
25
+ // InfoTip — accessible info-icon help affordance (RFC facebook/astryx#3349)
26
+ export {InfoTip, type InfoTipProps, type InfoTipSize} from './InfoTip';
27
+
25
28
  // Chat — experimental reasoning display
26
29
  export {
27
30
  ChatReasoning,