@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.
- package/dist/InfoTip/index.d.ts +21 -3
- package/dist/InfoTip/index.d.ts.map +1 -1
- package/dist/InfoTip/index.js +22 -2
- package/dist/lab.css +0 -3
- package/package.json +5 -5
- package/src/InfoTip/InfoTip.test.tsx +24 -229
- package/src/InfoTip/index.ts +28 -3
- package/src/MobilePowerSearch/MobilePowerSearch.test.tsx +3 -0
- package/src/MobileTokenizer/MobileTokenizer.test.tsx +3 -0
- package/src/Schedule/__tests__/ScheduleEventPopover.a11y.chromium.spec.ts +59 -23
- package/dist/InfoTip/InfoTip.d.ts +0 -58
- package/dist/InfoTip/InfoTip.d.ts.map +0 -1
- package/dist/InfoTip/InfoTip.js +0 -94
- package/src/InfoTip/InfoTip.doc.mjs +0 -100
- package/src/InfoTip/InfoTip.tsx +0 -128
package/dist/InfoTip/index.d.ts
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file InfoTip
|
|
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
|
-
|
|
5
|
-
|
|
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
|
|
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"}
|
package/dist/InfoTip/index.js
CHANGED
|
@@ -3,6 +3,26 @@
|
|
|
3
3
|
'use client';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* @file InfoTip
|
|
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
|
+
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.
|
|
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.
|
|
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.
|
|
63
|
-
"@astryxdesign/cli": "0.7.7-canary.
|
|
64
|
-
"@astryxdesign/interaction-harness": "0.7.7-canary.
|
|
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
|
|
6
|
-
* @output
|
|
7
|
-
* @position Testing; validates
|
|
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 {
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import {InfoTip} from '
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
});
|
package/src/InfoTip/index.ts
CHANGED
|
@@ -3,8 +3,33 @@
|
|
|
3
3
|
'use client';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* @file InfoTip
|
|
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
|
-
|
|
10
|
-
|
|
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,
|
|
447
|
-
//
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
461
|
-
|
|
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
|
|
464
|
-
|
|
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
|
|
474
|
-
await
|
|
475
|
-
await settle();
|
|
510
|
+
const afterPageDown = await pressUntilEnd('PageDown');
|
|
511
|
+
await press('Home');
|
|
476
512
|
const afterHome = await scroll();
|
|
477
|
-
const afterArrowDown = await
|
|
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"}
|
package/dist/InfoTip/InfoTip.js
DELETED
|
@@ -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
|
-
};
|
package/src/InfoTip/InfoTip.tsx
DELETED
|
@@ -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';
|