@astryxdesign/lab 0.7.7-canary.eadd175 → 0.7.7-canary.f4edef8

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.
@@ -1,6 +1,24 @@
1
1
  /**
2
- * @file InfoTip component barrel export
2
+ * @file InfoTip deprecated re-export
3
+ * @input `@astryxdesign/core/InfoTip`
4
+ * @output Exports InfoTip, InfoTipProps, InfoTipSize, each deprecated
5
+ * @position Lab entry kept for one release after InfoTip moved to Core;
6
+ * consumed by packages/lab/src/index.ts
3
7
  */
4
- export { InfoTip } from './InfoTip.js';
5
- export type { InfoTipProps, InfoTipSize } from './InfoTip.js';
8
+ import { InfoTip as CoreInfoTip, type InfoTipProps as CoreInfoTipProps, type InfoTipSize as CoreInfoTipSize } from '@astryxdesign/core/InfoTip';
9
+ /**
10
+ * @deprecated InfoTip moved to Core: import it from `@astryxdesign/core`.
11
+ * This Lab export is removed in the next release.
12
+ */
13
+ export declare const InfoTip: typeof CoreInfoTip;
14
+ /**
15
+ * @deprecated Import `InfoTipProps` from `@astryxdesign/core`. This Lab
16
+ * export is removed in the next release.
17
+ */
18
+ export type InfoTipProps = CoreInfoTipProps;
19
+ /**
20
+ * @deprecated Import `InfoTipSize` from `@astryxdesign/core`. This Lab export
21
+ * is removed in the next release.
22
+ */
23
+ export type InfoTipSize = CoreInfoTipSize;
6
24
  //# sourceMappingURL=index.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/InfoTip/index.ts"],"names":[],"mappings":"AAIA;;;;;;GAMG;AAEH,OAAO,EACL,OAAO,IAAI,WAAW,EACtB,KAAK,YAAY,IAAI,gBAAgB,EACrC,KAAK,WAAW,IAAI,eAAe,EACpC,MAAM,4BAA4B,CAAC;AAEpC;;;GAGG;AACH,eAAO,MAAM,OAAO,oBAAc,CAAC;AAEnC;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,gBAAgB,CAAC;AAE5C;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,eAAe,CAAC"}
@@ -3,6 +3,26 @@
3
3
  'use client';
4
4
 
5
5
  /**
6
- * @file InfoTip component barrel export
6
+ * @file InfoTip deprecated re-export
7
+ * @input `@astryxdesign/core/InfoTip`
8
+ * @output Exports InfoTip, InfoTipProps, InfoTipSize, each deprecated
9
+ * @position Lab entry kept for one release after InfoTip moved to Core;
10
+ * consumed by packages/lab/src/index.ts
7
11
  */
8
- export { InfoTip } from "./InfoTip.js";
12
+ import { InfoTip as CoreInfoTip } from '@astryxdesign/core/InfoTip';
13
+
14
+ /**
15
+ * @deprecated InfoTip moved to Core: import it from `@astryxdesign/core`.
16
+ * This Lab export is removed in the next release.
17
+ */
18
+ export const InfoTip = CoreInfoTip;
19
+
20
+ /**
21
+ * @deprecated Import `InfoTipProps` from `@astryxdesign/core`. This Lab
22
+ * export is removed in the next release.
23
+ */
24
+
25
+ /**
26
+ * @deprecated Import `InfoTipSize` from `@astryxdesign/core`. This Lab export
27
+ * is removed in the next release.
28
+ */
package/dist/lab.css CHANGED
@@ -71,7 +71,6 @@
71
71
  .x1fx55se:not(#\#){padding:calc((var(--size-element-lg) - 28px) / 2 - var(--border-width))}
72
72
  .xnvfkzk:not(#\#){padding:calc((var(--size-element-md) - 24px) / 2 - var(--border-width))}
73
73
  .x5pt662:not(#\#){padding:calc((var(--size-element-sm) - 22px) / 2 - var(--border-width))}
74
- .x12nba7r:not(#\#){padding:var(--spacing-0-5)}
75
74
  .x1b2ylru:not(#\#){padding:var(--spacing-3)}
76
75
  .x1shk3sm:not(#\#){padding:var(--spacing-4)}
77
76
  .x70o2i5:not(#\#):not(#\#){border-color:#26262a}
@@ -563,7 +562,6 @@
563
562
  .x80gvsz:not(#\#):not(#\#):not(#\#){transition-duration:var(--duration-medium)}
564
563
  .x15406qy:not(#\#):not(#\#):not(#\#){transition-property:background-color}
565
564
  .xq90yva:not(#\#):not(#\#):not(#\#){transition-property:border-color,background-color}
566
- .xt3l3uh:not(#\#):not(#\#):not(#\#){transition-property:color}
567
565
  .x13b0p5u:not(#\#):not(#\#):not(#\#){transition-property:none}
568
566
  .x19991ni:not(#\#):not(#\#):not(#\#){transition-property:opacity}
569
567
  .x1gfj591:not(#\#):not(#\#):not(#\#){transition-property:top,left,width,height}
@@ -635,7 +633,6 @@
635
633
  @media (hover: hover){.x1fkayqs.x1fkayqs.x1fkayqs:hover:where(:not(:disabled,[aria-disabled="true"])):not(#\#):not(#\#):not(#\#){background-color:#1d1d21}}
636
634
  @media (hover: hover){.xkqrm7g.xkqrm7g.xkqrm7g:hover:where(:not(:disabled,[aria-disabled="true"])):not(#\#):not(#\#):not(#\#){background-color:var(--color-background-muted)}}
637
635
  @media (hover: hover){.xoevpu5.xoevpu5.xoevpu5:hover:where(:not(:disabled,[aria-disabled="true"])):not(#\#):not(#\#):not(#\#){background-color:var(--color-overlay-hover)}}
638
- @media (hover: hover){.x10ue8fs.x10ue8fs.x10ue8fs:hover:where(:not(:disabled,[aria-disabled="true"])):not(#\#):not(#\#):not(#\#){color:var(--color-icon-primary)}}
639
636
  @container (max-width: 40rem){.x101j7pr.x101j7pr:not(#\#):not(#\#):not(#\#){background-color:var(--color-background-body)}}
640
637
  @container (max-width: 40rem){.x12eanku.x12eanku:not(#\#):not(#\#):not(#\#){border-inline-start-width:0}}
641
638
  @container list-input (max-width: 640px){.x1jiamoa.x1jiamoa:not(#\#):not(#\#):not(#\#){display:block}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/lab",
3
- "version": "0.7.7-canary.eadd175",
3
+ "version": "0.7.7-canary.f4edef8",
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",
@@ -49,7 +49,7 @@
49
49
  },
50
50
  "peerDependencies": {
51
51
  "@stylexjs/stylex": ">=0.10.0",
52
- "@astryxdesign/core": "0.7.7-canary.eadd175",
52
+ "@astryxdesign/core": "0.7.7-canary.f4edef8",
53
53
  "react": ">=19.0.0",
54
54
  "react-dom": ">=19.0.0"
55
55
  },
@@ -59,9 +59,9 @@
59
59
  "d3-shape": "^3.2.0"
60
60
  },
61
61
  "devDependencies": {
62
- "@astryxdesign/a11y-spec": "0.7.7-canary.eadd175",
63
- "@astryxdesign/cli": "0.7.7-canary.eadd175",
64
- "@astryxdesign/interaction-harness": "0.7.7-canary.eadd175",
62
+ "@astryxdesign/a11y-spec": "0.7.7-canary.f4edef8",
63
+ "@astryxdesign/cli": "0.7.7-canary.f4edef8",
64
+ "@astryxdesign/interaction-harness": "0.7.7-canary.f4edef8",
65
65
  "@babel/cli": "^7.29.7",
66
66
  "@babel/core": "^7.29.7",
67
67
  "@babel/preset-react": "^7.29.7",
@@ -2,237 +2,32 @@
2
2
 
3
3
  /**
4
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
5
+ * @input The Lab InfoTip entry and the Core component
6
+ * @output Tests that the Lab entry re-exports the Core component, deprecated
7
+ * @position Testing; validates index.ts (component:InfoTip FR4)
10
8
  */
11
9
 
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;
10
+ import {readFileSync} from 'node:fs';
11
+ import path from 'node:path';
12
+ import {describe, expect, it} from 'vitest';
13
+ import {InfoTip as CoreInfoTip} from '@astryxdesign/core/InfoTip';
14
+ import {InfoTip} from './index';
15
+
16
+ describe('Lab InfoTip', () => {
17
+ it('is the Core component', () => {
18
+ expect(InfoTip).toBe(CoreInfoTip);
19
+ });
20
+
21
+ it('marks each of its exports deprecated', () => {
22
+ // Static metadata only: editors and type checkers show it, and nothing
23
+ // warns at runtime.
24
+ const source = readFileSync(path.join(__dirname, 'index.ts'), 'utf8');
25
+ for (const name of ['InfoTip', 'InfoTipProps', 'InfoTipSize']) {
26
+ // The doc comment right above the export, whatever it contains.
27
+ const comment = new RegExp(
28
+ String.raw`/\*\*((?:(?!\*/)[\s\S])*)\*/\nexport (?:const|type) ${name}\b`,
29
+ ).exec(source);
30
+ expect(comment?.[1], name).toContain('@deprecated');
44
31
  }
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
- describe('touch', () => {
170
- /**
171
- * A tap, in the order a browser dispatches it: the finger's pointer events,
172
- * the hover it synthesizes, then the `click` that says the touch was a tap.
173
- */
174
- const tap = (element: HTMLElement) => {
175
- fireEvent.pointerEnter(element, {pointerType: 'touch'});
176
- fireEvent.pointerDown(element, {pointerType: 'touch'});
177
- fireEvent.pointerUp(element, {pointerType: 'touch'});
178
- fireEvent.mouseEnter(element);
179
- fireEvent.click(element);
180
- };
181
-
182
- it('opens on a tap, though the trigger is a button', async () => {
183
- render(<InfoTip content="30-day rolling average." />);
184
- const trigger = screen.getByRole('button');
185
- const layer = screen.getByRole('tooltip', {hidden: true});
186
-
187
- // Tooltip's `auto` rule would give this tap to the button and suppress
188
- // the tooltip; revealing it is the button's whole purpose, so InfoTip
189
- // opts in. Without this its content is unreachable on a phone.
190
- tap(trigger);
191
-
192
- await waitFor(() => {
193
- expect(popoverOpenState.get(layer)).toBe(true);
194
- });
195
- });
196
-
197
- it('closes on a second tap of the trigger', async () => {
198
- render(<InfoTip content="30-day rolling average." />);
199
- const trigger = screen.getByRole('button');
200
- const layer = screen.getByRole('tooltip', {hidden: true});
201
-
202
- tap(trigger);
203
- await waitFor(() => {
204
- expect(popoverOpenState.get(layer)).toBe(true);
205
- });
206
-
207
- tap(trigger);
208
- await waitFor(() => {
209
- expect(popoverOpenState.get(layer)).toBe(false);
210
- });
211
- });
212
-
213
- it('closes on a tap outside', async () => {
214
- render(
215
- <>
216
- <InfoTip content="30-day rolling average." />
217
- <button type="button">Elsewhere</button>
218
- </>,
219
- );
220
- const trigger = screen.getByRole('button', {name: 'More information'});
221
- const layer = screen.getByRole('tooltip', {hidden: true});
222
-
223
- tap(trigger);
224
- await waitFor(() => {
225
- expect(popoverOpenState.get(layer)).toBe(true);
226
- });
227
-
228
- // The tap outside counts when the finger lifts.
229
- const elsewhere = screen.getByRole('button', {name: 'Elsewhere'});
230
- fireEvent.pointerDown(elsewhere, {pointerType: 'touch'});
231
- fireEvent.pointerUp(elsewhere, {pointerType: 'touch'});
232
-
233
- await waitFor(() => {
234
- expect(popoverOpenState.get(layer)).toBe(false);
235
- });
236
- });
237
32
  });
238
33
  });
@@ -3,8 +3,33 @@
3
3
  'use client';
4
4
 
5
5
  /**
6
- * @file InfoTip component barrel export
6
+ * @file InfoTip deprecated re-export
7
+ * @input `@astryxdesign/core/InfoTip`
8
+ * @output Exports InfoTip, InfoTipProps, InfoTipSize, each deprecated
9
+ * @position Lab entry kept for one release after InfoTip moved to Core;
10
+ * consumed by packages/lab/src/index.ts
7
11
  */
8
12
 
9
- export {InfoTip} from './InfoTip';
10
- export type {InfoTipProps, InfoTipSize} from './InfoTip';
13
+ import {
14
+ InfoTip as CoreInfoTip,
15
+ type InfoTipProps as CoreInfoTipProps,
16
+ type InfoTipSize as CoreInfoTipSize,
17
+ } from '@astryxdesign/core/InfoTip';
18
+
19
+ /**
20
+ * @deprecated InfoTip moved to Core: import it from `@astryxdesign/core`.
21
+ * This Lab export is removed in the next release.
22
+ */
23
+ export const InfoTip = CoreInfoTip;
24
+
25
+ /**
26
+ * @deprecated Import `InfoTipProps` from `@astryxdesign/core`. This Lab
27
+ * export is removed in the next release.
28
+ */
29
+ export type InfoTipProps = CoreInfoTipProps;
30
+
31
+ /**
32
+ * @deprecated Import `InfoTipSize` from `@astryxdesign/core`. This Lab export
33
+ * is removed in the next release.
34
+ */
35
+ export type InfoTipSize = CoreInfoTipSize;
@@ -822,6 +822,9 @@ describe('MobilePowerSearch Filters sheet', () => {
822
822
  const confirmation = screen.getByRole('alertdialog', {
823
823
  name: 'Clear all filters?',
824
824
  });
825
+ // A scrim press starts and ends on the dialog itself.
826
+ fireEvent.pointerDown(confirmation);
827
+ fireEvent.pointerUp(confirmation);
825
828
  fireEvent.click(confirmation);
826
829
  expect(
827
830
  screen.queryByRole('alertdialog', {name: 'Clear all filters?'}),
@@ -185,6 +185,9 @@ describe('MobileTokenizer (Lab, single-sheet flow)', () => {
185
185
  let discard = await screen.findByRole('alertdialog', {
186
186
  name: 'Discard changes?',
187
187
  });
188
+ // A scrim press starts and ends on the dialog itself.
189
+ fireEvent.pointerDown(discard);
190
+ fireEvent.pointerUp(discard);
188
191
  fireEvent.click(discard);
189
192
  await waitFor(() =>
190
193
  expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument(),
@@ -443,38 +443,74 @@ test('long popover content scrolls from the keyboard: Tab reaches its named grou
443
443
  top: Math.round(element.scrollTop),
444
444
  max: element.scrollHeight - element.clientHeight,
445
445
  }));
446
- // Keyboard scrolling is smooth, so each press is followed until the region
447
- // settles; the key is pressed until a press no longer moves it.
448
- const settle = async () => {
449
- let last = -1;
450
- for (let frame = 0; frame < 40; frame += 1) {
451
- const {top} = await scroll();
452
- if (top === last) {
453
- return top;
454
- }
455
- last = top;
456
- await page.waitForTimeout(50);
457
- }
458
- return last;
446
+ // Keyboard scrolling is smooth, and a busy browser can start it frames after
447
+ // the press, so each press is followed to the region's `scrollend`. A press
448
+ // that starts no scroll within 60 frames of its keydown is one that cannot
449
+ // move the region.
450
+ const press = async (key: string) => {
451
+ await region.evaluate(element => {
452
+ (window as unknown as {scrolled?: Promise<boolean>}).scrolled =
453
+ new Promise<boolean>(resolve => {
454
+ const listening = new AbortController();
455
+ const end = (scrolled: boolean) => {
456
+ listening.abort();
457
+ resolve(scrolled);
458
+ };
459
+ let started = false;
460
+ element.addEventListener(
461
+ 'scroll',
462
+ () => {
463
+ started = true;
464
+ },
465
+ {signal: listening.signal},
466
+ );
467
+ element.addEventListener('scrollend', () => end(true), {
468
+ signal: listening.signal,
469
+ });
470
+ // The frames count from the key's own keydown, so a slow runner
471
+ // between this setup and the press does not use them up.
472
+ window.addEventListener(
473
+ 'keydown',
474
+ () => {
475
+ let frames = 0;
476
+ const wait = () => {
477
+ if (started || listening.signal.aborted) {
478
+ return;
479
+ }
480
+ frames += 1;
481
+ if (frames > 60) {
482
+ end(false);
483
+ return;
484
+ }
485
+ requestAnimationFrame(wait);
486
+ };
487
+ requestAnimationFrame(wait);
488
+ },
489
+ {capture: true, once: true, signal: listening.signal},
490
+ );
491
+ });
492
+ });
493
+ await page.keyboard.press(key);
494
+ return page.evaluate(
495
+ () => (window as unknown as {scrolled?: Promise<boolean>}).scrolled,
496
+ );
459
497
  };
460
- const pressUntilStill = async (key: string) => {
461
- let top = await settle();
498
+ // The key is pressed until the region reaches its end or a press no longer
499
+ // moves it.
500
+ const pressUntilEnd = async (key: string) => {
462
501
  for (let presses = 0; presses < 200; presses += 1) {
463
- await page.keyboard.press(key);
464
- const next = await settle();
465
- if (next === top) {
502
+ const {top, max} = await scroll();
503
+ if (top >= max - 1 || !(await press(key))) {
466
504
  break;
467
505
  }
468
- top = next;
469
506
  }
470
507
  return scroll();
471
508
  };
472
509
  const atStart = await scroll();
473
- const afterPageDown = await pressUntilStill('PageDown');
474
- await page.keyboard.press('Home');
475
- await settle();
510
+ const afterPageDown = await pressUntilEnd('PageDown');
511
+ await press('Home');
476
512
  const afterHome = await scroll();
477
- const afterArrowDown = await pressUntilStill('ArrowDown');
513
+ const afterArrowDown = await pressUntilEnd('ArrowDown');
478
514
  // The surface itself stays inside the viewport.
479
515
  const surface = await dialog.boundingBox();
480
516
  const viewportHeight = page.viewportSize()?.height ?? 0;
@@ -1,58 +0,0 @@
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 meta/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, keyboard focus, and tap. Use it next to labels, values,
39
- * and 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
@@ -1 +0,0 @@
1
- {"version":3,"file":"InfoTip.d.ts","sourceRoot":"","sources":["../../src/InfoTip/InfoTip.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AAEH,OAAO,EAAC,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAGrC,OAAO,EAAO,KAAK,QAAQ,EAAC,MAAM,yBAAyB,CAAC;AAW5D;;;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;AA8BD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,OAAO,CAAC,EACtB,OAAO,EACP,KAA0B,EAC1B,IAAW,GACZ,EAAE,YAAY,GAAG,SAAS,CAqB1B;yBAzBe,OAAO"}
@@ -1,94 +0,0 @@
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 meta/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 * as stylex from '@stylexjs/stylex';
17
- import { Icon } from '@astryxdesign/core/Icon';
18
- import { Tooltip } from '@astryxdesign/core/Tooltip';
19
- import '@astryxdesign/core/theme/tokens.stylex';
20
- import { colorVars, durationVars, easeVars, radiusVars, spacingVars } from '@astryxdesign/core/theme/tokens.stylex';
21
- import { focusOutlineStyles } from '@astryxdesign/core/utils';
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
- const styles = {
29
- trigger: {
30
- k1xSpc: "x3nfvp2",
31
- kGNEyG: "x6s0dn4",
32
- kjj79g: "xl56j7k",
33
- kXLuUW: "xxymvpz",
34
- kmVPX3: "x12nba7r",
35
- kogj98: "x1ghz6dp",
36
- ksu8eU: "xng3xce",
37
- kWkggS: "xjbqb8w",
38
- kaIpWk: "xjspbzw",
39
- kkrTdU: "x1ypdohk x16khyan",
40
- kMwMTN: "xv9yike x10ue8fs",
41
- k1ekBW: "xt3l3uh",
42
- kIyJzY: "xuedmi6",
43
- kAMwcw: "xlr8y92",
44
- $$css: true
45
- }
46
- };
47
-
48
- /**
49
- * An inline info-icon help affordance: a small "i" button that reveals a
50
- * tooltip on hover, keyboard focus, and tap. Use it next to labels, values,
51
- * and metrics for permission notes, metric definitions, and field help.
52
- *
53
- * The value over hand-composing Icon inside Tooltip is the pre-wired
54
- * accessible trigger: a real button with an aria-label, Tab-reachable,
55
- * tooltip on hover AND focus, and Escape dismissal.
56
- *
57
- * Composed entirely from core primitives (Tooltip + Icon); the info icon
58
- * resolves from the global icon registry, so themes can override it.
59
- *
60
- * @example
61
- * ```
62
- * <InfoTip content="Editors can change this field; viewers cannot." />
63
- * <InfoTip content="30-day rolling average." label="About this metric" />
64
- * ```
65
- */
66
- export function InfoTip({
67
- content,
68
- label = 'More information',
69
- size = 'sm'
70
- }) {
71
- // No local Escape handler: the trigger used to stopPropagation the press,
72
- // which hid the tip but left the browser's close watcher to fire `cancel` on
73
- // an enclosing Dialog, so one press took both (#5168). Tooltip's own entry in
74
- // the shared dismissal stack takes it instead.
75
- return /*#__PURE__*/_jsx(Tooltip, {
76
- content: content
77
- // The trigger is a real button, so Tooltip's `auto` touch rule would
78
- // give the tap to the control and suppress the tooltip. Here the tooltip
79
- // IS the control's only purpose, so the tap has to open it — otherwise
80
- // an InfoTip's content is unreachable on a phone.
81
- ,
82
- touchTrigger: "tap",
83
- children: /*#__PURE__*/_jsx("button", {
84
- type: "button",
85
- "aria-label": label,
86
- ...stylex.props(focusOutlineStyles.focusVisible, styles.trigger),
87
- children: /*#__PURE__*/_jsx(Icon, {
88
- icon: "info",
89
- size: size
90
- })
91
- })
92
- });
93
- }
94
- InfoTip.displayName = 'InfoTip';
@@ -1,100 +0,0 @@
1
- // Copyright (c) Meta Platforms, Inc. and affiliates.
2
-
3
- /** @type {import('@astryxdesign/cli/authoring').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, keyboard focus, and tap. 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 tap, 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: true, description: 'Rely on InfoTip rather than a hand-rolled info button on touch: it sets Tooltip touchTrigger to tap, so a tap opens the tooltip even though the trigger is a button, and a tap outside dismisses it.' },
37
- { 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.' },
38
- { guidance: false, description: 'Use InfoTip for essential information users must see to complete a task; put that text inline instead.' },
39
- ],
40
- },
41
- };
42
-
43
- /** @type {import('@astryxdesign/cli/authoring').ComponentDoc} */
44
- export const docsZh = {
45
- name: 'InfoTip',
46
- displayName: 'Info Tip',
47
- props: [
48
- {
49
- name: 'content',
50
- type: 'ReactNode',
51
- description: '工具提示中显示的内容。通常为简短的非交互文本。与 Tooltip 的 content 属性一致。',
52
- required: true,
53
- },
54
- {
55
- name: 'label',
56
- type: 'string',
57
- description: '触发按钮的无障碍名称。',
58
- default: "'More information'",
59
- },
60
- {
61
- name: 'size',
62
- type: "'xsm' | 'sm' | 'md' | 'lg'",
63
- description: '信息图标的尺寸(12px、16px、20px、24px)。与 Icon 尺寸一一对应。',
64
- default: "'sm'",
65
- },
66
- ],
67
- usage: {
68
- description:
69
- 'An inline info-icon help affordance: a small "i" button that reveals a tooltip on hover, keyboard focus, and tap. Use it next to labels, values, and metrics for permission notes, metric definitions, and field help.',
70
- bestPractices: [
71
- { guidance: true, description: 'Keep tooltip content concise, plain, and non-interactive; use Popover or HoverCard for links and buttons.' },
72
- { guidance: true, description: 'Override label when "More information" is too generic, e.g. "About this metric".' },
73
- { guidance: true, description: 'Rely on InfoTip rather than a hand-rolled info button on touch: it sets Tooltip touchTrigger to tap, so a tap opens the tooltip even though the trigger is a button, and a tap outside dismisses it.' },
74
- { 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.' },
75
- { guidance: false, description: 'Use InfoTip for essential information users must see to complete a task; put that text inline instead.' },
76
- ],
77
- },
78
- };
79
-
80
- /** @type {import('@astryxdesign/cli/authoring').ComponentTranslationDoc} */
81
- export const docsDense = {
82
- description:
83
- 'Inline info-icon help affordance: accessible "i" button trigger pre-wired into a Tooltip.',
84
- usage: {
85
- description:
86
- 'A small "i" button that reveals a tooltip on hover, keyboard focus, and tap. 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 AND tap, Escape dismissal.',
87
- bestPractices: [
88
- { guidance: true, description: 'Keep tooltip content concise, plain, and non-interactive; use Popover or HoverCard for links and buttons.' },
89
- { guidance: true, description: 'Override label when "More information" is too generic, e.g. "About this metric".' },
90
- { guidance: true, description: 'Rely on InfoTip rather than a hand-rolled info button on touch: it sets Tooltip touchTrigger to tap, so a tap opens the tooltip even though the trigger is a button, and a tap outside dismisses it.' },
91
- { guidance: false, description: 'Hand-compose Icon inside Tooltip for info affordances; the bare Icon is aria-hidden and unfocusable.' },
92
- { guidance: false, description: 'Use InfoTip for essential information users must see to complete a task; put that text inline instead.' },
93
- ],
94
- },
95
- propDescriptions: {
96
- content: 'Tooltip content. Short, non-interactive text. Mirrors Tooltip content prop.',
97
- label: "Accessible name for the trigger button. Default 'More information'.",
98
- size: 'Info icon size (12/16/20/24px). Maps 1:1 to Icon sizes.',
99
- },
100
- };
@@ -1,128 +0,0 @@
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 meta/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 {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
- import {focusOutlineStyles} from '@astryxdesign/core/utils';
30
-
31
- /**
32
- * Size of the info icon. Maps 1:1 to Icon sizes
33
- * (xsm: 12px, sm: 16px, md: 20px, lg: 24px).
34
- */
35
- export type InfoTipSize = IconSize;
36
-
37
- export interface InfoTipProps {
38
- /**
39
- * Content to display in the tooltip.
40
- * Typically short, non-interactive text. Mirrors Tooltip's `content` prop.
41
- */
42
- content: ReactNode;
43
- /**
44
- * Accessible name for the trigger button.
45
- * @default 'More information'
46
- */
47
- label?: string;
48
- /**
49
- * Size of the info icon.
50
- * @default 'sm'
51
- */
52
- size?: InfoTipSize;
53
- }
54
-
55
- const styles = stylex.create({
56
- trigger: {
57
- display: 'inline-flex',
58
- alignItems: 'center',
59
- justifyContent: 'center',
60
- verticalAlign: 'middle',
61
- padding: spacingVars['--spacing-0-5'],
62
- margin: 0,
63
- borderStyle: 'none',
64
- backgroundColor: 'transparent',
65
- borderRadius: radiusVars['--radius-full'],
66
- cursor: {
67
- default: 'pointer',
68
- ':is(:disabled,[aria-disabled="true"])': 'default',
69
- },
70
- color: {
71
- default: colorVars['--color-icon-secondary'],
72
- ':hover:where(:not(:disabled,[aria-disabled="true"]))': {
73
- default: null,
74
- '@media (hover: hover)': colorVars['--color-icon-primary'],
75
- },
76
- },
77
- transitionProperty: 'color',
78
- transitionDuration: durationVars['--duration-fast'],
79
- transitionTimingFunction: easeVars['--ease-standard'],
80
- },
81
- });
82
-
83
- /**
84
- * An inline info-icon help affordance: a small "i" button that reveals a
85
- * tooltip on hover, keyboard focus, and tap. Use it next to labels, values,
86
- * and metrics for permission notes, metric definitions, and field help.
87
- *
88
- * The value over hand-composing Icon inside Tooltip is the pre-wired
89
- * accessible trigger: a real button with an aria-label, Tab-reachable,
90
- * tooltip on hover AND focus, and Escape dismissal.
91
- *
92
- * Composed entirely from core primitives (Tooltip + Icon); the info icon
93
- * resolves from the global icon registry, so themes can override it.
94
- *
95
- * @example
96
- * ```
97
- * <InfoTip content="Editors can change this field; viewers cannot." />
98
- * <InfoTip content="30-day rolling average." label="About this metric" />
99
- * ```
100
- */
101
- export function InfoTip({
102
- content,
103
- label = 'More information',
104
- size = 'sm',
105
- }: InfoTipProps): ReactNode {
106
- // No local Escape handler: the trigger used to stopPropagation the press,
107
- // which hid the tip but left the browser's close watcher to fire `cancel` on
108
- // an enclosing Dialog, so one press took both (#5168). Tooltip's own entry in
109
- // the shared dismissal stack takes it instead.
110
- return (
111
- <Tooltip
112
- content={content}
113
- // The trigger is a real button, so Tooltip's `auto` touch rule would
114
- // give the tap to the control and suppress the tooltip. Here the tooltip
115
- // IS the control's only purpose, so the tap has to open it — otherwise
116
- // an InfoTip's content is unreachable on a phone.
117
- touchTrigger="tap">
118
- <button
119
- type="button"
120
- aria-label={label}
121
- {...stylex.props(focusOutlineStyles.focusVisible, styles.trigger)}>
122
- <Icon icon="info" size={size} />
123
- </button>
124
- </Tooltip>
125
- );
126
- }
127
-
128
- InfoTip.displayName = 'InfoTip';