@astryxdesign/core 0.7.0 → 0.7.1-canary.0365e23

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.
Files changed (80) hide show
  1. package/dist/BottomSheet/BottomSheet.d.ts +1 -1
  2. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  3. package/dist/BottomSheet/BottomSheet.js +12 -2
  4. package/dist/CodeBlock/CodeBlock.d.ts +10 -1
  5. package/dist/CodeBlock/CodeBlock.d.ts.map +1 -1
  6. package/dist/CodeBlock/CodeBlock.js +158 -38
  7. package/dist/CodeBlock/tokenizer.d.ts +39 -0
  8. package/dist/CodeBlock/tokenizer.d.ts.map +1 -1
  9. package/dist/CodeBlock/tokenizer.js +108 -0
  10. package/dist/ContextMenu/ContextMenu.d.ts +1 -1
  11. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  12. package/dist/ContextMenu/ContextMenu.js +22 -8
  13. package/dist/Dialog/Dialog.d.ts.map +1 -1
  14. package/dist/Dialog/Dialog.js +3 -26
  15. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  16. package/dist/DropdownMenu/DropdownMenu.js +10 -3
  17. package/dist/Markdown/Markdown.d.ts.map +1 -1
  18. package/dist/Markdown/Markdown.js +19 -3
  19. package/dist/Typeahead/BaseTypeahead.d.ts +1 -1
  20. package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
  21. package/dist/Typeahead/BaseTypeahead.js +12 -3
  22. package/dist/astryx.css +13 -0
  23. package/dist/hooks/useIsomorphicLayoutEffect.d.ts +2 -2
  24. package/dist/hooks/useIsomorphicLayoutEffect.d.ts.map +1 -1
  25. package/dist/theme/syntax/SyntaxTheme.d.ts.map +1 -1
  26. package/dist/theme/syntax/SyntaxTheme.js +43 -2
  27. package/dist/theme/syntax/defineSyntaxTheme.d.ts +18 -5
  28. package/dist/theme/syntax/defineSyntaxTheme.d.ts.map +1 -1
  29. package/dist/theme/syntax/defineSyntaxTheme.js +38 -9
  30. package/dist/theme/syntax/index.d.ts +1 -1
  31. package/dist/theme/syntax/index.d.ts.map +1 -1
  32. package/dist/theme/syntax/tokens.d.ts +12 -2
  33. package/dist/theme/syntax/tokens.d.ts.map +1 -1
  34. package/dist/theme/syntax/tokens.js +25 -4
  35. package/dist/utils/focusReturn.d.ts +12 -8
  36. package/dist/utils/focusReturn.d.ts.map +1 -1
  37. package/dist/utils/focusReturn.js +44 -3
  38. package/package.json +4 -4
  39. package/src/BottomSheet/BottomSheet.test.tsx +114 -0
  40. package/src/BottomSheet/BottomSheet.tsx +12 -2
  41. package/src/BottomSheet/__tests__/BottomSheetFocusReturn.a11y.browser.spec.ts +96 -0
  42. package/src/Chat/__tests__/ChatLayoutScrollButtonSurface.a11y.chromium.spec.ts +5 -6
  43. package/src/CodeBlock/CodeBlock.doc.mjs +7 -0
  44. package/src/CodeBlock/CodeBlock.spec.md +55 -45
  45. package/src/CodeBlock/CodeBlock.test.tsx +89 -9
  46. package/src/CodeBlock/CodeBlock.tsx +205 -25
  47. package/src/CodeBlock/__tests__/CodeBlockOverflowStop.a11y.chromium.spec.ts +125 -0
  48. package/src/CodeBlock/__tests__/DiffRowPaint.a11y.chromium.spec.ts +93 -0
  49. package/src/CodeBlock/tokenizer.test.ts +112 -0
  50. package/src/CodeBlock/tokenizer.ts +106 -0
  51. package/src/ContextMenu/ContextMenu.spec.md +1 -1
  52. package/src/ContextMenu/ContextMenu.test.tsx +36 -3
  53. package/src/ContextMenu/ContextMenu.tsx +27 -11
  54. package/src/ContextMenu/__tests__/ContextMenuCursorAnchor.a11y.browser.spec.ts +153 -0
  55. package/src/Dialog/Dialog.tsx +2 -30
  56. package/src/Dialog/__tests__/Dialog.a11y.chromium.spec.ts +21 -14
  57. package/src/DropdownMenu/DropdownMenu.test.tsx +97 -1
  58. package/src/DropdownMenu/DropdownMenu.tsx +10 -3
  59. package/src/DropdownMenu/__tests__/DropdownMenuFocusReturn.a11y.browser.spec.ts +73 -0
  60. package/src/FieldStatus/__tests__/StatusMessage.a11y.chromium.spec.ts +21 -14
  61. package/src/Markdown/Markdown.spec.md +37 -23
  62. package/src/Markdown/Markdown.test.tsx +38 -0
  63. package/src/Markdown/Markdown.tsx +20 -2
  64. package/src/Markdown/plugins/semanticFence.test.tsx +3 -2
  65. package/src/NumberInput/__tests__/NumberInput.a11y.chromium.spec.ts +22 -15
  66. package/src/PowerSearch/PowerSearch.test.tsx +45 -0
  67. package/src/Switch/__tests__/Switch.a11y.chromium.spec.ts +25 -18
  68. package/src/TabList/__tests__/Tabs.a11y.chromium.spec.ts +19 -12
  69. package/src/TextInput/__tests__/TextInput.a11y.chromium.spec.ts +21 -14
  70. package/src/ToggleButton/__tests__/ToggleButton.a11y.chromium.spec.ts +18 -11
  71. package/src/Typeahead/BaseTypeahead.test.tsx +99 -1
  72. package/src/Typeahead/BaseTypeahead.tsx +17 -3
  73. package/src/theme/syntax/SyntaxTheme.tsx +46 -5
  74. package/src/theme/syntax/defineSyntaxTheme.diff.test.ts +91 -0
  75. package/src/theme/syntax/defineSyntaxTheme.ts +75 -15
  76. package/src/theme/syntax/index.ts +5 -1
  77. package/src/theme/syntax/tokens.ts +32 -2
  78. package/src/theme/syntax/useSyntaxTheme.diff.test.tsx +40 -0
  79. package/src/utils/focusReturn.test.ts +79 -2
  80. package/src/utils/focusReturn.ts +48 -3
@@ -11,7 +11,11 @@
11
11
  * @see https://github.com/facebook/astryx/issues/1148
12
12
  */
13
13
 
14
- import {syntaxTokenDefaults, type SyntaxTokenName} from './tokens';
14
+ import {
15
+ syntaxTokenDefaults,
16
+ DIFF_SYNTAX_TOKEN_NAMES,
17
+ type SyntaxTokenName,
18
+ } from './tokens';
15
19
  import {devWarn} from '../../utils/devWarning';
16
20
 
17
21
  // =============================================================================
@@ -33,7 +37,12 @@ export type SyntaxThemeTokenKey =
33
37
  | 'attribute'
34
38
  | 'property'
35
39
  | 'punctuation'
36
- | 'background';
40
+ | 'background'
41
+ | DiffSyntaxThemeTokenKey;
42
+
43
+ /** The diff row slots a syntax theme MAY set; unset ones keep their defaults. */
44
+ export type DiffSyntaxThemeTokenKey =
45
+ 'added' | 'removed' | 'diff-header' | 'added-marker' | 'removed-marker';
37
46
 
38
47
  /**
39
48
  * Token value — either a single string or a [light, dark] tuple.
@@ -41,14 +50,28 @@ export type SyntaxThemeTokenKey =
41
50
  */
42
51
  export type SyntaxTokenValue = string | [light: string, dark: string];
43
52
 
44
- /** Token map for defineSyntaxTheme input — values can be strings or tuples. */
53
+ /**
54
+ * Token map for defineSyntaxTheme input — values can be strings or tuples.
55
+ * The diff row slots are optional: a theme that sets none keeps the defaults
56
+ * (semantic success/error muted tints and the muted surface).
57
+ */
45
58
  export type SyntaxThemeTokenInput = Record<
46
- SyntaxThemeTokenKey,
59
+ Exclude<SyntaxThemeTokenKey, DiffSyntaxThemeTokenKey>,
47
60
  SyntaxTokenValue
48
- >;
61
+ > &
62
+ Partial<Record<DiffSyntaxThemeTokenKey, SyntaxTokenValue>>;
49
63
 
50
- /** Resolved token map — all values are CSS strings (tuples resolved to light-dark()). */
51
- export type SyntaxThemeTokenMap = Record<SyntaxThemeTokenKey, string>;
64
+ /**
65
+ * Resolved token map — all values are CSS strings (tuples resolved to
66
+ * light-dark()). The diff row slots are optional here as in the input, so a
67
+ * map written before they existed still type-checks; an unset slot renders
68
+ * its default.
69
+ */
70
+ export type SyntaxThemeTokenMap = Record<
71
+ Exclude<SyntaxThemeTokenKey, DiffSyntaxThemeTokenKey>,
72
+ string
73
+ > &
74
+ Partial<Record<DiffSyntaxThemeTokenKey, string>>;
52
75
 
53
76
  /** Input to defineSyntaxTheme. */
54
77
  export interface SyntaxThemeInput {
@@ -81,6 +104,22 @@ export const ALL_SYNTAX_KEYS: SyntaxThemeTokenKey[] = Object.keys(
81
104
  syntaxTokenDefaults,
82
105
  ).map(k => k.replace(CSS_PREFIX, '') as SyntaxThemeTokenKey);
83
106
 
107
+ const DIFF_KEYS = new Set<SyntaxThemeTokenKey>(
108
+ DIFF_SYNTAX_TOKEN_NAMES.map(
109
+ name => name.replace(CSS_PREFIX, '') as SyntaxThemeTokenKey,
110
+ ),
111
+ );
112
+
113
+ /** The marker slots: unset in a theme, they follow that theme's punctuation. */
114
+ const MARKER_KEYS = new Set<SyntaxThemeTokenKey>([
115
+ 'added-marker',
116
+ 'removed-marker',
117
+ ]);
118
+
119
+ /** The keys every syntax theme must set: everything but the diff rows. */
120
+ export const REQUIRED_SYNTAX_KEYS: SyntaxThemeTokenKey[] =
121
+ ALL_SYNTAX_KEYS.filter(key => !DIFF_KEYS.has(key));
122
+
84
123
  // =============================================================================
85
124
  // Helpers
86
125
  // =============================================================================
@@ -138,19 +177,37 @@ export function resolveSyntaxTokenForMode(
138
177
  export function defineSyntaxTheme(
139
178
  input: SyntaxThemeInput,
140
179
  ): SyntaxThemeDefinition {
141
- const missing = ALL_SYNTAX_KEYS.filter(key => !(key in input.tokens));
180
+ const missing = REQUIRED_SYNTAX_KEYS.filter(key => !(key in input.tokens));
142
181
  if (missing.length > 0) {
143
182
  devWarn(
144
183
  'defineSyntaxTheme',
145
184
  `"${input.name}": missing tokens: ${missing.join(', ')}. ` +
146
- `All 14 syntax tokens are required.`,
185
+ `All ${REQUIRED_SYNTAX_KEYS.length} syntax colour tokens are required; ` +
186
+ `the diff row slots (added, removed, diff-header, added-marker, removed-marker) are optional.`,
147
187
  );
148
188
  }
149
189
 
150
- // Resolve tuples to light-dark() CSS strings
190
+ // Resolve tuples to light-dark() CSS strings. An unset diff row paint keeps
191
+ // its default so every existing theme renders a readable diff unchanged; an
192
+ // unset marker slot takes this theme's own line-number colour (its
193
+ // punctuation token) rather than the page theme's green and red, and an
194
+ // unset header paint is none at all (the header row sits on this theme's
195
+ // own background with punctuation text), so number, glyph, and header
196
+ // text stay legible on this theme's background in every page mode — the
197
+ // glyph's shape carries the meaning where the colour does not.
151
198
  const resolved: Partial<SyntaxThemeTokenMap> = {};
152
199
  for (const key of ALL_SYNTAX_KEYS) {
153
- resolved[key] = resolveTokenValue(input.tokens[key]);
200
+ const value = (
201
+ input.tokens as Partial<Record<SyntaxThemeTokenKey, SyntaxTokenValue>>
202
+ )[key];
203
+ resolved[key] =
204
+ value !== undefined
205
+ ? resolveTokenValue(value)
206
+ : MARKER_KEYS.has(key)
207
+ ? 'var(--color-syntax-punctuation)'
208
+ : key === 'diff-header'
209
+ ? 'transparent'
210
+ : syntaxTokenDefaults[toCSSProperty(key)];
154
211
  }
155
212
 
156
213
  return {
@@ -170,14 +227,17 @@ export function syntaxThemeStyle(
170
227
  ): Record<string, string> {
171
228
  const vars: Record<string, string> = {};
172
229
  for (const key of ALL_SYNTAX_KEYS) {
173
- vars[toCSSProperty(key)] = theme.tokens[key];
230
+ // An unset diff slot is left to its default rather than written empty.
231
+ const value = theme.tokens[key];
232
+ if (value !== undefined) vars[toCSSProperty(key)] = value;
174
233
  }
175
234
  return vars;
176
235
  }
177
236
 
178
237
  /** Convert a syntax theme to CSS declarations (no selector wrapper). */
179
238
  export function syntaxThemeToCSS(theme: SyntaxThemeDefinition): string {
180
- return ALL_SYNTAX_KEYS.map(
181
- key => toCSSProperty(key) + ': ' + theme.tokens[key] + ';',
182
- ).join('\n ');
239
+ // An unset diff slot is left to its default rather than written `undefined`.
240
+ return ALL_SYNTAX_KEYS.filter(key => theme.tokens[key] !== undefined)
241
+ .map(key => toCSSProperty(key) + ': ' + theme.tokens[key] + ';')
242
+ .join('\n ');
183
243
  }
@@ -14,7 +14,11 @@
14
14
  export {syntaxTokenDefaults} from './tokens';
15
15
  export type {SyntaxTokenName} from './tokens';
16
16
 
17
- export {defineSyntaxTheme, syntaxThemeStyle, syntaxThemeToCSS} from './defineSyntaxTheme';
17
+ export {
18
+ defineSyntaxTheme,
19
+ syntaxThemeStyle,
20
+ syntaxThemeToCSS,
21
+ } from './defineSyntaxTheme';
18
22
  export type {
19
23
  SyntaxThemeDefinition,
20
24
  SyntaxThemeInput,
@@ -13,8 +13,11 @@
13
13
  * colors automatically adapt to any theme's color system. Themes can also
14
14
  * set an explicit syntax theme via defineTheme({ syntax: dracula }).
15
15
  *
16
- * 14-token architecture validated against 11 community code themes.
17
- * All themes map cleanly to these 14 slots.
16
+ * 14 token colours and the code background, validated against 11 community
17
+ * code themes — all of them map cleanly to those slots — plus five diff row
18
+ * slots (added, removed, header paints; added and removed markers) that default
19
+ * to semantic tokens, so a syntax theme that does not set them keeps a
20
+ * readable diff.
18
21
  *
19
22
  * @see https://github.com/facebook/astryx/issues/1148
20
23
  */
@@ -51,6 +54,33 @@ export const syntaxTokenDefaults = {
51
54
  '--color-syntax-punctuation': 'var(--color-text-secondary)',
52
55
  // background -> muted surface
53
56
  '--color-syntax-background': 'var(--color-background-muted)',
57
+ // Diff rows (CodeBlock isDiff): whole-row tints behind unchanged syntax
58
+ // colours. Added/removed derive from the semantic success/error muted
59
+ // tokens at 25% strength: at full strength the secondary-grey syntax
60
+ // tokens (operator, punctuation, comment) fall under 4.5:1 on the tints
61
+ // in light; at 25% every default token clears it on both schemes with
62
+ // margin while the tint still reads against the plain row. Header rows
63
+ // (@@, and ---/+++ before the first hunk) sit on the muted surface.
64
+ '--color-syntax-added':
65
+ 'color-mix(in srgb, var(--color-success-muted) 25%, transparent)',
66
+ '--color-syntax-removed':
67
+ 'color-mix(in srgb, var(--color-error-muted) 25%, transparent)',
68
+ '--color-syntax-diff-header': 'var(--color-background-muted)',
69
+ // The margin of a changed row — its line number and +/- glyph — in the
70
+ // default theme's tinted-text tokens, made for tinted surfaces; a defined
71
+ // syntax theme that leaves them unset follows its own punctuation colour
72
+ // (see defineSyntaxTheme). Code text keeps the syntax theme's colours.
73
+ '--color-syntax-added-marker': 'var(--color-text-green)',
74
+ '--color-syntax-removed-marker': 'var(--color-text-red)',
54
75
  } as const;
55
76
 
56
77
  export type SyntaxTokenName = keyof typeof syntaxTokenDefaults;
78
+
79
+ /** The five diff row slots: optional in a syntax theme, defaulted when unset. */
80
+ export const DIFF_SYNTAX_TOKEN_NAMES = [
81
+ '--color-syntax-added',
82
+ '--color-syntax-removed',
83
+ '--color-syntax-diff-header',
84
+ '--color-syntax-added-marker',
85
+ '--color-syntax-removed-marker',
86
+ ] as const satisfies ReadonlyArray<SyntaxTokenName>;
@@ -0,0 +1,40 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file useSyntaxTheme.diff.test.tsx
5
+ * @input A SyntaxTheme provider with a preset that sets no diff slots
6
+ * @output The hook hands a canvas consumer paintable colours for every slot
7
+ * @position Diff slot resolution for the imperative (canvas) path
8
+ */
9
+
10
+ import {render} from '@testing-library/react';
11
+ import {describe, expect, it} from 'vitest';
12
+ import {SyntaxTheme, useSyntaxTheme} from './SyntaxTheme';
13
+ import {dracula} from './presets';
14
+ import {parseColor} from '../../utils/color';
15
+
16
+ function Probe({onTokens}: {onTokens: (t: Record<string, string>) => void}) {
17
+ const syntax = useSyntaxTheme();
18
+ if (syntax) onTokens(syntax.tokens);
19
+ return null;
20
+ }
21
+
22
+ describe('useSyntaxTheme diff slots', () => {
23
+ it("resolves unset marker slots to the theme's own punctuation colour", () => {
24
+ let tokens: Record<string, string> = {};
25
+ render(
26
+ <SyntaxTheme theme={dracula}>
27
+ <Probe onTokens={t => (tokens = t)} />
28
+ </SyntaxTheme>,
29
+ );
30
+ expect(tokens.punctuation).toMatch(/^#|^rgb/);
31
+ expect(tokens['added-marker']).toBe(tokens.punctuation);
32
+ expect(tokens['removed-marker']).toBe(tokens.punctuation);
33
+ // The row paints derive from the page theme's resolved muted tokens at
34
+ // the default strength, as rgba() a canvas can paint.
35
+ expect(tokens.added).toMatch(/^rgba\(\d+, \d+, \d+, 0\.\d+\)$/);
36
+ expect(tokens.removed).toMatch(/^rgba\(\d+, \d+, \d+, 0\.\d+\)$/);
37
+ expect(parseColor(tokens.added)?.a).toBeCloseTo(0.2 * 0.25, 3);
38
+ expect(tokens['diff-header']).toBe('transparent');
39
+ });
40
+ });
@@ -1,7 +1,11 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
- import {describe, it, expect, afterEach} from 'vitest';
4
- import {isFocusDetached} from './focusReturn';
3
+ import {describe, it, expect, afterEach, beforeEach, vi} from 'vitest';
4
+ import {focusForLastInput, isFocusDetached} from './focusReturn';
5
+ import {
6
+ __resetInteractionModalityForTest,
7
+ __startInteractionModalityTrackingForTest,
8
+ } from './interactionModality';
5
9
 
6
10
  describe('isFocusDetached', () => {
7
11
  afterEach(() => {
@@ -40,3 +44,76 @@ describe('isFocusDetached', () => {
40
44
  expect(isFocusDetached(doc)).toBe(true);
41
45
  });
42
46
  });
47
+
48
+ describe('focusForLastInput', () => {
49
+ beforeEach(() => {
50
+ __startInteractionModalityTrackingForTest();
51
+ __resetInteractionModalityForTest();
52
+ });
53
+
54
+ afterEach(() => {
55
+ vi.restoreAllMocks();
56
+ document.body.innerHTML = '';
57
+ });
58
+
59
+ function button(): HTMLButtonElement {
60
+ const element = document.createElement('button');
61
+ document.body.appendChild(element);
62
+ return element;
63
+ }
64
+
65
+ it('leaves the ring to the browser after keyboard input, passing the options through', () => {
66
+ const element = button();
67
+ const focus = vi.spyOn(element, 'focus');
68
+ focusForLastInput(element);
69
+ focusForLastInput(element, {preventScroll: true});
70
+ expect(focus.mock.calls).toEqual([[], [{preventScroll: true}]]);
71
+ });
72
+
73
+ it('asks the browser for no ring after pointer input, keeping the options', () => {
74
+ const element = button();
75
+ document.dispatchEvent(new Event('pointerdown'));
76
+ const focus = vi.spyOn(element, 'focus');
77
+ focusForLastInput(element);
78
+ focusForLastInput(element, {preventScroll: true});
79
+ expect(focus.mock.calls).toEqual([
80
+ [{focusVisible: false}],
81
+ [{preventScroll: true, focusVisible: false}],
82
+ ]);
83
+ });
84
+
85
+ it('keeps plain focus on a text field after pointer input', () => {
86
+ const input = document.createElement('input');
87
+ document.body.appendChild(input);
88
+ document.dispatchEvent(new Event('pointerdown'));
89
+ const focus = vi.spyOn(input, 'focus');
90
+ focusForLastInput(input, {preventScroll: true});
91
+ expect(focus.mock.calls).toEqual([[{preventScroll: true}]]);
92
+ });
93
+
94
+ it('clears a ring the browser already drew before refocusing after pointer input', () => {
95
+ const element = button();
96
+ element.focus();
97
+ document.dispatchEvent(new Event('pointerdown'));
98
+ const matches = HTMLElement.prototype.matches;
99
+ vi.spyOn(HTMLElement.prototype, 'matches').mockImplementation(function (
100
+ this: HTMLElement,
101
+ selector: string,
102
+ ) {
103
+ return selector === ':focus-visible'
104
+ ? this === element
105
+ : matches.call(this, selector);
106
+ });
107
+ const blur = vi.spyOn(element, 'blur');
108
+ const focus = vi.spyOn(element, 'focus');
109
+ focusForLastInput(element, {preventScroll: true});
110
+ expect(blur).toHaveBeenCalledTimes(1);
111
+ expect(focus.mock.calls).toEqual([
112
+ [{preventScroll: true, focusVisible: false}],
113
+ ]);
114
+ expect(blur.mock.invocationCallOrder[0]).toBeLessThan(
115
+ focus.mock.invocationCallOrder[0] ?? 0,
116
+ );
117
+ expect(document.activeElement).toBe(element);
118
+ });
119
+ });
@@ -2,13 +2,18 @@
2
2
 
3
3
  /**
4
4
  * @file focusReturn.ts
5
- * @input The document's current active element.
6
- * @output Whether focus is currently "detached" — on nothing in particular.
5
+ * @input The document's current active element and the shared interaction
6
+ * modality.
7
+ * @output Whether focus is currently "detached" — on nothing in particular —
8
+ * and a focus move whose ring follows the last input.
7
9
  * @position Utility; used by overlay triggers (e.g. DateInput, DateTimeInput)
8
10
  * that return focus to their control when the overlay closes, but only when
9
- * the dismiss did not already move focus somewhere the user chose.
11
+ * the dismiss did not already move focus somewhere the user chose, and by
12
+ * overlays (Dialog, DropdownMenu) that move focus programmatically.
10
13
  */
11
14
 
15
+ import {getInteractionModality} from './interactionModality';
16
+
12
17
  /**
13
18
  * True when focus rests on nothing in particular — no active element, or the
14
19
  * document body / root element.
@@ -29,3 +34,43 @@ export function isFocusDetached(doc: Document = document): boolean {
29
34
  active == null || active === doc.body || active === doc.documentElement
30
35
  );
31
36
  }
37
+
38
+ /**
39
+ * Move focus programmatically so its ring follows the last input
40
+ * (`architecture:interaction-modality` INV1). After pointer input the browser's
41
+ * focus option keeps the element from matching `:focus-visible`, which is the
42
+ * one way to reach an element the overlay does not render: the consumer's
43
+ * autofocus target, the control that opened a Dialog, or a menu trigger the
44
+ * caller renders. Pass `options` (for example `preventScroll`) as to `focus()`.
45
+ *
46
+ * Make sure modality tracking is active (`useInteractionModalityTracking()`)
47
+ * in the component that calls this.
48
+ */
49
+ export function focusForLastInput(
50
+ element: HTMLElement,
51
+ options?: FocusOptions,
52
+ ): void {
53
+ // A text-entry field paints its own focus treatment to show where typing
54
+ // goes, and an extra blur there can run validation or mark it touched.
55
+ const isTextEntry =
56
+ element instanceof HTMLInputElement ||
57
+ element instanceof HTMLTextAreaElement ||
58
+ element instanceof HTMLSelectElement ||
59
+ element.isContentEditable;
60
+ if (isTextEntry || getInteractionModality() !== 'pointer') {
61
+ if (options === undefined) {
62
+ element.focus();
63
+ } else {
64
+ element.focus(options);
65
+ }
66
+ return;
67
+ }
68
+ // Focusing the element that already has focus does nothing, so a ring the
69
+ // browser already drew would stay. WebKit draws one when its own focus
70
+ // restoration (a dialog's close, a popover's hide) moves focus itself, so
71
+ // clear that focus first.
72
+ if (element.matches(':focus-visible')) {
73
+ element.blur();
74
+ }
75
+ element.focus({...options, focusVisible: false});
76
+ }