@atlaskit/top-layer 0.15.0 → 0.17.0
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/CHANGELOG.md +45 -0
- package/README.md +4 -4
- package/dist/cjs/animations/presets.js +96 -9
- package/dist/cjs/dialog/dialog-content.js +33 -4
- package/dist/cjs/dialog-scroll-lock/index.js +17 -7
- package/dist/cjs/entry-points/animations.js +12 -0
- package/dist/cjs/entry-points/use-popover-id.js +3 -1
- package/dist/cjs/internal/get-aria-for-trigger.js +8 -3
- package/dist/cjs/internal/use-anchor-position-at-point.js +4 -2
- package/dist/cjs/internal/use-anchor-position.js +3 -2
- package/dist/cjs/internal/use-animated-visibility.js +233 -155
- package/dist/cjs/internal/use-focus-wrap.js +5 -3
- package/dist/cjs/internal/use-initial-focus.js +196 -50
- package/dist/cjs/internal/use-width-from-anchor.js +5 -4
- package/dist/cjs/popover/popover.js +58 -9
- package/dist/es2019/animations/presets.js +145 -2
- package/dist/es2019/dialog/dialog-content.js +33 -4
- package/dist/es2019/dialog-scroll-lock/index.js +18 -8
- package/dist/es2019/entry-points/animations.js +1 -1
- package/dist/es2019/entry-points/use-popover-id.js +3 -1
- package/dist/es2019/internal/get-aria-for-trigger.js +8 -3
- package/dist/es2019/internal/use-anchor-position-at-point.js +4 -2
- package/dist/es2019/internal/use-anchor-position.js +3 -2
- package/dist/es2019/internal/use-animated-visibility.js +222 -149
- package/dist/es2019/internal/use-focus-wrap.js +5 -4
- package/dist/es2019/internal/use-initial-focus.js +189 -45
- package/dist/es2019/internal/use-width-from-anchor.js +5 -4
- package/dist/es2019/popover/popover.js +59 -10
- package/dist/esm/animations/presets.js +93 -9
- package/dist/esm/dialog/dialog-content.js +33 -4
- package/dist/esm/dialog-scroll-lock/index.js +17 -8
- package/dist/esm/entry-points/animations.js +1 -1
- package/dist/esm/entry-points/use-popover-id.js +3 -1
- package/dist/esm/internal/get-aria-for-trigger.js +8 -3
- package/dist/esm/internal/use-anchor-position-at-point.js +4 -2
- package/dist/esm/internal/use-anchor-position.js +3 -2
- package/dist/esm/internal/use-animated-visibility.js +235 -157
- package/dist/esm/internal/use-focus-wrap.js +5 -4
- package/dist/esm/internal/use-initial-focus.js +196 -51
- package/dist/esm/internal/use-width-from-anchor.js +5 -4
- package/dist/esm/popover/popover.js +59 -10
- package/dist/types/animations/presets.d.ts +16 -0
- package/dist/types/dialog/types.d.ts +9 -1
- package/dist/types/dialog-scroll-lock/index.d.ts +22 -6
- package/dist/types/entry-points/animations.d.ts +1 -1
- package/dist/types/internal/get-aria-for-trigger.d.ts +8 -1
- package/dist/types/internal/use-anchor-position-at-point.d.ts +6 -1
- package/dist/types/internal/use-anchor-position.d.ts +14 -1
- package/dist/types/internal/use-animated-visibility.d.ts +48 -19
- package/dist/types/internal/use-focus-wrap.d.ts +6 -1
- package/dist/types/internal/use-initial-focus.d.ts +55 -4
- package/dist/types/internal/use-width-from-anchor.d.ts +3 -2
- package/dist/types/popover/types.d.ts +13 -2
- package/dist/types-ts4.5/animations/presets.d.ts +16 -0
- package/dist/types-ts4.5/dialog/types.d.ts +9 -1
- package/dist/types-ts4.5/dialog-scroll-lock/index.d.ts +22 -6
- package/dist/types-ts4.5/entry-points/animations.d.ts +1 -1
- package/dist/types-ts4.5/internal/get-aria-for-trigger.d.ts +8 -1
- package/dist/types-ts4.5/internal/use-anchor-position-at-point.d.ts +6 -1
- package/dist/types-ts4.5/internal/use-anchor-position.d.ts +14 -1
- package/dist/types-ts4.5/internal/use-animated-visibility.d.ts +48 -19
- package/dist/types-ts4.5/internal/use-focus-wrap.d.ts +6 -1
- package/dist/types-ts4.5/internal/use-initial-focus.d.ts +55 -4
- package/dist/types-ts4.5/internal/use-width-from-anchor.d.ts +3 -2
- package/dist/types-ts4.5/popover/types.d.ts +13 -2
- package/package.json +3 -3
|
@@ -29,16 +29,51 @@ type TUseAnimatedVisibilityArgs = {
|
|
|
29
29
|
};
|
|
30
30
|
type TUseAnimatedVisibilityResult = {
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* Current visibility phase. See `TPhase` for the full lifecycle.
|
|
33
|
+
*
|
|
34
|
+
* `phase !== 'closed'` means the host element is mounted and on
|
|
35
|
+
* screen, including during animated entry, animated exit, and the
|
|
36
|
+
* non-animated close handshake. Use this when you need "the user
|
|
37
|
+
* can still see / interact with the element" semantics (e.g. focus
|
|
38
|
+
* trapping, listeners that must outlive the exit).
|
|
34
39
|
*/
|
|
35
|
-
|
|
40
|
+
phase: TPhase;
|
|
36
41
|
/**
|
|
37
42
|
* Resolved animation preset (with CSS injected), or `null` if
|
|
38
43
|
* animation is disabled.
|
|
39
44
|
*/
|
|
40
45
|
preset: TAnimationPreset | null;
|
|
41
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* Visibility lifecycle phase shared between `useAnimatedVisibility`,
|
|
49
|
+
* `useFocusWrap`, `useInitialFocus`, and the `Popover` / `Dialog`
|
|
50
|
+
* components.
|
|
51
|
+
*
|
|
52
|
+
* - `closed`: host element is unmounted. No DOM presence.
|
|
53
|
+
* - `entering`: host element is mounted and the CSS entry transition
|
|
54
|
+
* is playing. **Only emitted when animation is enabled.** With
|
|
55
|
+
* animation disabled the lifecycle goes directly `closed → open`.
|
|
56
|
+
* - `open`: host element is mounted and settled. No transition is
|
|
57
|
+
* playing.
|
|
58
|
+
* - `exiting`: `isOpen` has flipped to `false` but the host element
|
|
59
|
+
* is still mounted. Emitted while the CSS exit transition is playing
|
|
60
|
+
* (animation enabled) OR while we wait for the browser's
|
|
61
|
+
* `toggle`/`close` event to fire on the still-attached element so
|
|
62
|
+
* focus restoration can run (animation disabled).
|
|
63
|
+
*
|
|
64
|
+
* State machine:
|
|
65
|
+
* ```
|
|
66
|
+
* animation ON: closed → entering → open → exiting → closed
|
|
67
|
+
* animation OFF: closed → open → exiting → closed
|
|
68
|
+
* interrupts: entering → exiting (close mid-entry)
|
|
69
|
+
* exiting → entering (reopen mid-exit, animated)
|
|
70
|
+
* exiting → open (reopen mid-handshake, non-animated)
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* `phase !== 'closed'` is the canonical "host is currently in the DOM
|
|
74
|
+
* and on screen" predicate.
|
|
75
|
+
*/
|
|
76
|
+
export type TPhase = 'closed' | 'entering' | 'open' | 'exiting';
|
|
42
77
|
/**
|
|
43
78
|
* Manages the children mount/unmount lifecycle around CSS exit transitions.
|
|
44
79
|
*
|
|
@@ -55,26 +90,20 @@ type TUseAnimatedVisibilityResult = {
|
|
|
55
90
|
*
|
|
56
91
|
* ## How it works
|
|
57
92
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
93
|
+
* The lifecycle is modeled as a `phase` discriminated union (`closed`,
|
|
94
|
+
* `entering`, `open`, `exiting`). The host element is rendered while
|
|
95
|
+
* `phase !== 'closed'`, so the `exiting` phase keeps the host mounted
|
|
96
|
+
* while the CSS exit transition plays (or, with animation disabled,
|
|
97
|
+
* while we wait for the browser `toggle` / `close` event to fire on the
|
|
98
|
+
* still-attached element):
|
|
61
99
|
*
|
|
62
100
|
* ```
|
|
63
|
-
* isOpen:
|
|
64
|
-
*
|
|
101
|
+
* isOpen: true ──────────────── false
|
|
102
|
+
* phase: open ──────────────── exiting ─── (exit settle) ─── closed
|
|
65
103
|
* ```
|
|
66
104
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* 1. **Animated close** (`willAnimate === true`):
|
|
70
|
-
* `isOpen` goes `false` → `showChildren` stays `true` → the CSS exit
|
|
71
|
-
* transition plays → `transitionend` fires → `showChildren` → `false`
|
|
72
|
-
* → `onExitFinish` fires. A timeout fallback ensures we still unmount
|
|
73
|
-
* if `transitionend` never fires (e.g. browser quirks, zero-duration).
|
|
74
|
-
*
|
|
75
|
-
* 2. **Non-animated close** (`willAnimate === false`):
|
|
76
|
-
* `isOpen` goes `false` → `showChildren` → `false` synchronously during
|
|
77
|
-
* the same render → `onExitFinish` fires in a follow-up effect.
|
|
105
|
+
* Every effect dispatches on the single `phase` value instead of a
|
|
106
|
+
* cross-product of booleans.
|
|
78
107
|
*/
|
|
79
108
|
export declare function useAnimatedVisibility({ isOpen, animate, elementRef, onEnterFinish, onExitFinish, }: TUseAnimatedVisibilityArgs): TUseAnimatedVisibilityResult;
|
|
80
109
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type RefObject } from 'react';
|
|
2
|
+
import { type TPhase } from './use-animated-visibility';
|
|
2
3
|
/**
|
|
3
4
|
* Wraps Tab/Shift+Tab focus within a popover element when its role
|
|
4
5
|
* semantically requires it (dialog, alertdialog).
|
|
@@ -14,7 +15,11 @@ import { type RefObject } from 'react';
|
|
|
14
15
|
* attached when the role requires focus wrapping, and is cleaned
|
|
15
16
|
* up and re-evaluated when the role changes.
|
|
16
17
|
*/
|
|
17
|
-
export declare function useFocusWrap({ elementRef, role, }: {
|
|
18
|
+
export declare function useFocusWrap({ elementRef, role, phase, }: {
|
|
18
19
|
elementRef: RefObject<HTMLElement | null>;
|
|
19
20
|
role: string | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Whether the element is open. Used to attach the Tab listener.
|
|
23
|
+
*/
|
|
24
|
+
phase: TPhase;
|
|
20
25
|
}): void;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type RefObject } from 'react';
|
|
2
|
+
import { type TPhase } from './use-animated-visibility';
|
|
2
3
|
/**
|
|
3
4
|
* Moves focus into a popover element when it opens, based on its ARIA role.
|
|
4
5
|
*
|
|
@@ -12,11 +13,61 @@ import { type RefObject } from 'react';
|
|
|
12
13
|
* Role is taken from the typed `role` prop - this is the consumer's contract
|
|
13
14
|
* and is not subject to drift from a directly-mutated DOM attribute.
|
|
14
15
|
*
|
|
15
|
-
* Always call this hook unconditionally. Focus is
|
|
16
|
-
*
|
|
16
|
+
* Always call this hook unconditionally. Focus is moved on each transition
|
|
17
|
+
* into a visible phase, the moment the host element first becomes visible
|
|
18
|
+
* for that open intent:
|
|
19
|
+
*
|
|
20
|
+
* - Animated path: on `closed → entering`. The host has just been mounted
|
|
21
|
+
* and the layout effect that calls `showPopover()` / `showModal()` has
|
|
22
|
+
* already run, so the element is in the top layer. We focus before the
|
|
23
|
+
* entry transition settles to match the WAI-ARIA APG expectation that
|
|
24
|
+
* focus lands on the new menu / dialog the instant it opens, not after
|
|
25
|
+
* the CSS finishes animating.
|
|
26
|
+
* - Non-animated path: on `closed → open` (the entering phase is skipped).
|
|
27
|
+
* - Reopen mid-exit: on `exiting → entering`. When a user dismisses the
|
|
28
|
+
* popup (Escape, close button, light dismiss) the browser natively
|
|
29
|
+
* restores focus to the trigger as part of `hidePopover()` /
|
|
30
|
+
* `<dialog>.close()`. If the consumer reopens before the exit transition
|
|
31
|
+
* has settled, the phase machine jumps `exiting → entering` without
|
|
32
|
+
* passing through `closed`, so without this branch the popup would be
|
|
33
|
+
* visible again with focus stranded on the trigger — a keyboard usability
|
|
34
|
+
* gap for menu/dialog/listbox roles.
|
|
35
|
+
*
|
|
36
|
+
* Note: there is a rare programmatic path where a consumer flips `isOpen`
|
|
37
|
+
* false → true without any user-initiated close gesture and had moved focus
|
|
38
|
+
* to a non-default target inside the popup. In that scenario this hook
|
|
39
|
+
* re-focuses the role-appropriate default (e.g. first menu item) rather
|
|
40
|
+
* than preserving the consumer-chosen target. This matches the documented
|
|
41
|
+
* `closed → entering` behavior and is considered acceptable given how
|
|
42
|
+
* abstract the scenario is. See `notes/architecture/focus.md`.
|
|
43
|
+
*
|
|
44
|
+
* Pure `entering → exiting → entering` stutters where the popup never
|
|
45
|
+
* reaches `closed` and the consumer toggles in the same frame are still
|
|
46
|
+
* collapsed: the `prevPhase` ref guard rejects any transition whose
|
|
47
|
+
* previous phase was not `closed` or `exiting`.
|
|
48
|
+
*
|
|
49
|
+
* Tradeoff vs the previous `requestAnimationFrame` based approach: we no
|
|
50
|
+
* longer wait one paint before focusing. The element is already attached
|
|
51
|
+
* and in the top layer when this effect runs (the show / hide layout
|
|
52
|
+
* effect runs first), so screen-reader announcement and visible focus
|
|
53
|
+
* land together. If a regression appears that needs the extra paint
|
|
54
|
+
* (e.g. browsers that require a layout pass before `focus()` works on a
|
|
55
|
+
* freshly promoted top-layer element), wrap the `focus()` call in a
|
|
56
|
+
* single `requestAnimationFrame` here and re-add a same-element ref
|
|
57
|
+
* check inside the RAF callback.
|
|
17
58
|
*/
|
|
18
|
-
export declare function useInitialFocus({ elementRef,
|
|
59
|
+
export declare function useInitialFocus({ elementRef, phase, role, }: {
|
|
19
60
|
elementRef: RefObject<HTMLElement | null>;
|
|
20
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Current visibility phase from `useAnimatedVisibility`. Initial
|
|
63
|
+
* focus is moved on the transition out of `'closed'`:
|
|
64
|
+
*
|
|
65
|
+
* - Animated path: fires on `closed → entering`, immediately after
|
|
66
|
+
* the host element mounts and `showPopover()` / `showModal()` has
|
|
67
|
+
* run in the preceding layout effect.
|
|
68
|
+
* - Non-animated path: fires on `closed → open` (entering phase is
|
|
69
|
+
* skipped entirely when animation is disabled).
|
|
70
|
+
*/
|
|
71
|
+
phase: TPhase;
|
|
21
72
|
role: string | undefined;
|
|
22
73
|
}): void;
|
|
@@ -13,8 +13,9 @@ import type { TWidthFromAnchorMode } from '../popover/types';
|
|
|
13
13
|
*
|
|
14
14
|
* Designed to be composed alongside `useAnchorPosition` for top-layer popovers.
|
|
15
15
|
*/
|
|
16
|
-
export declare function useWidthFromAnchor({ mode, popoverRef, anchorRef, }: {
|
|
16
|
+
export declare function useWidthFromAnchor({ mode, popoverRef, anchorRef, isOpen, }: {
|
|
17
17
|
mode: TWidthFromAnchorMode;
|
|
18
18
|
popoverRef: RefObject<HTMLElement | null>;
|
|
19
|
-
anchorRef: RefObject<HTMLElement | null
|
|
19
|
+
anchorRef: RefObject<HTMLElement | null>;
|
|
20
|
+
isOpen: boolean;
|
|
20
21
|
}): void;
|
|
@@ -65,8 +65,19 @@ type TPopoverBaseProps = {
|
|
|
65
65
|
* animation plays (via `allow-discrete`) before the popover becomes logically closed.
|
|
66
66
|
* Otherwise hides instantly.
|
|
67
67
|
*
|
|
68
|
-
* The
|
|
69
|
-
*
|
|
68
|
+
* The consumer does not conditionally render the `Popover` — visibility is driven
|
|
69
|
+
* by this prop.
|
|
70
|
+
*
|
|
71
|
+
* **Lifecycle observable to consumers:**
|
|
72
|
+
*
|
|
73
|
+
* - The host element is in the DOM only while the popover is open or its exit
|
|
74
|
+
* animation is playing. After exit completes the element is unmounted so it
|
|
75
|
+
* does not leave an empty `role` / `popover` element in the accessibility tree.
|
|
76
|
+
* The exact unmount timing is private and may change.
|
|
77
|
+
* - The `id` (supplied or generated via `usePopoverId()`) is stable across opens.
|
|
78
|
+
* - The `ref` is populated only while the host element is rendered. Consumers
|
|
79
|
+
* that read from the ref outside of `onOpenChange` / `onEnterFinish` should
|
|
80
|
+
* gate the read on `isOpen` being `true`.
|
|
70
81
|
*
|
|
71
82
|
* **Important:** For `mode="auto"` popovers, the browser can dismiss the popover
|
|
72
83
|
* via light dismiss (Escape, click outside) independently of this prop. When that
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type TAnimationPreset } from './types';
|
|
2
|
+
export declare function popupMotion(): TAnimationPreset;
|
|
2
3
|
type TSlideAndFadeOptions = {
|
|
3
4
|
/**
|
|
4
5
|
* Slide distance in pixels. Defaults to `4`.
|
|
@@ -51,6 +52,21 @@ export declare function fade(): TAnimationPreset;
|
|
|
51
52
|
* ```
|
|
52
53
|
*/
|
|
53
54
|
export declare function scaleAndFade(): TAnimationPreset;
|
|
55
|
+
/**
|
|
56
|
+
* Dialog motion animation.
|
|
57
|
+
*
|
|
58
|
+
* Includes backdrop fade animation.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```tsx
|
|
62
|
+
* import { dialogMotion } from '@atlaskit/top-layer/animations';
|
|
63
|
+
*
|
|
64
|
+
* <Dialog animate={dialogMotion()} isOpen={isOpen} onClose={handleClose} label="...">
|
|
65
|
+
* ...
|
|
66
|
+
* </Dialog>
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export declare function dialogMotion(): TAnimationPreset;
|
|
54
70
|
type TDialogSlideUpAndFadeOptions = {
|
|
55
71
|
/**
|
|
56
72
|
* Slide distance in pixels. Defaults to `12`.
|
|
@@ -26,7 +26,15 @@ type TDialogBaseProps = {
|
|
|
26
26
|
* - `false`: calls `close()`. When an `animate` preset is provided, the exit
|
|
27
27
|
* animation plays (via `allow-discrete`) before the dialog becomes logically closed.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
29
|
+
* **Lifecycle observable to consumers:**
|
|
30
|
+
*
|
|
31
|
+
* - The `<dialog>` host element is in the DOM only while open or its exit
|
|
32
|
+
* animation is playing. After exit completes the element is unmounted so
|
|
33
|
+
* it does not leave an empty `role="dialog"` element in the accessibility
|
|
34
|
+
* tree. The exact unmount timing is private and may change.
|
|
35
|
+
* - The `id` (supplied or generated) is stable across opens.
|
|
36
|
+
* - The `ref` is populated only while the host element is rendered. Consumers
|
|
37
|
+
* that read from the ref should gate the read on `isOpen` being `true`.
|
|
30
38
|
*/
|
|
31
39
|
isOpen: boolean;
|
|
32
40
|
/**
|
|
@@ -1,14 +1,29 @@
|
|
|
1
|
+
type TDialogScrollLockProps = {
|
|
2
|
+
/**
|
|
3
|
+
* Whether the parent dialog is open. The scroll lock is applied while
|
|
4
|
+
* `true` and released the moment this flips to `false`, regardless of
|
|
5
|
+
* when (or whether) React unmounts this component. Pass the same
|
|
6
|
+
* `isOpen` value you pass to the parent `Dialog`.
|
|
7
|
+
*/
|
|
8
|
+
isOpen: boolean;
|
|
9
|
+
};
|
|
1
10
|
/**
|
|
2
11
|
* __DialogScrollLock__
|
|
3
12
|
*
|
|
4
|
-
* Prevents background page scrolling while
|
|
5
|
-
* inside a `Dialog`
|
|
6
|
-
* dialog closes (unmounts its children).
|
|
13
|
+
* Prevents background page scrolling while a dialog is open. Designed to be
|
|
14
|
+
* rendered inside a `Dialog` and given the same `isOpen` value.
|
|
7
15
|
*
|
|
8
16
|
* Native `<dialog>.showModal()` makes background content _inert_ (not
|
|
9
17
|
* focusable/clickable), but does **not** prevent scroll events from reaching
|
|
10
18
|
* the document body. This component fills that gap by setting
|
|
11
|
-
* `overflow: hidden` on `document.body`.
|
|
19
|
+
* `overflow: hidden` on `document.body` while `isOpen` is `true`.
|
|
20
|
+
*
|
|
21
|
+
* The lock is driven by `isOpen` rather than by mount/unmount so that release
|
|
22
|
+
* happens synchronously in the same commit that flips `isOpen` to `false`.
|
|
23
|
+
* Relying on unmount alone was racy: `Dialog` defers the unmount of its
|
|
24
|
+
* children until the host element's close/toggle event fires, which can be
|
|
25
|
+
* after a consumer or test observes the dialog as hidden, leading to a
|
|
26
|
+
* stale `overflow: hidden` window.
|
|
12
27
|
*
|
|
13
28
|
* @example
|
|
14
29
|
* ```tsx
|
|
@@ -16,9 +31,10 @@
|
|
|
16
31
|
* import { DialogScrollLock } from '@atlaskit/top-layer/dialog-scroll-lock';
|
|
17
32
|
*
|
|
18
33
|
* <Dialog isOpen={isOpen} onClose={onClose}>
|
|
19
|
-
* <DialogScrollLock />
|
|
34
|
+
* <DialogScrollLock isOpen={isOpen} />
|
|
20
35
|
* {children}
|
|
21
36
|
* </Dialog>
|
|
22
37
|
* ```
|
|
23
38
|
*/
|
|
24
|
-
export declare function DialogScrollLock(): null;
|
|
39
|
+
export declare function DialogScrollLock({ isOpen }: TDialogScrollLockProps): null;
|
|
40
|
+
export {};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { slideAndFade, fade, scaleAndFade, dialogSlideUpAndFade, dialogFade, } from '../animations/presets';
|
|
1
|
+
export { dialogMotion, popupMotion, slideAndFade, fade, scaleAndFade, dialogSlideUpAndFade, dialogFade, } from '../animations/presets';
|
|
2
2
|
export type { TAnimationPreset } from '../animations/types';
|
|
@@ -33,7 +33,14 @@ type TAriaHasPopupForTrigger = Exclude<TAriaHasPopupValue, undefined>;
|
|
|
33
33
|
type TAriaForTrigger = {
|
|
34
34
|
'aria-haspopup': TAriaHasPopupForTrigger;
|
|
35
35
|
'aria-expanded': boolean;
|
|
36
|
-
|
|
36
|
+
/**
|
|
37
|
+
* `aria-controls` is `undefined` while the popover is closed, because
|
|
38
|
+
* the popover host element is only rendered in the DOM while open. A
|
|
39
|
+
* dangling `aria-controls` reference (pointing at a missing ID) is
|
|
40
|
+
* tolerated by assistive tech but flagged by some a11y tooling, so
|
|
41
|
+
* we leave it unset until the target exists.
|
|
42
|
+
*/
|
|
43
|
+
'aria-controls': string | undefined;
|
|
37
44
|
};
|
|
38
45
|
/**
|
|
39
46
|
* Returns the ARIA attributes to spread onto a click-activated popover trigger.
|
|
@@ -34,7 +34,7 @@ export type TAnchorPoint = {
|
|
|
34
34
|
* }
|
|
35
35
|
* ```
|
|
36
36
|
*/
|
|
37
|
-
export declare function useAnchorPositionAtPoint({ popoverRef, placement, getPoint, isEnabled, }: {
|
|
37
|
+
export declare function useAnchorPositionAtPoint({ popoverRef, placement, getPoint, isEnabled, isOpen, }: {
|
|
38
38
|
/**
|
|
39
39
|
* Element being positioned. Passed to `useAnchorPosition`.
|
|
40
40
|
*/
|
|
@@ -59,4 +59,9 @@ export declare function useAnchorPositionAtPoint({ popoverRef, placement, getPoi
|
|
|
59
59
|
* and no positioning is applied. Defaults to `true`.
|
|
60
60
|
*/
|
|
61
61
|
isEnabled?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Whether the popover is currently open. Forwarded to the inner
|
|
64
|
+
* `useAnchorPosition` so it re-runs across host remount cycles.
|
|
65
|
+
*/
|
|
66
|
+
isOpen: boolean;
|
|
62
67
|
}): void;
|
|
@@ -34,13 +34,19 @@ import { type TPlacementOptions } from '../internal/resolve-placement';
|
|
|
34
34
|
* comparison is used internally so re-runs are skipped when the resolved
|
|
35
35
|
* placement shape is unchanged.
|
|
36
36
|
*/
|
|
37
|
-
export declare function useAnchorPosition({ anchorRef, popoverRef, placement, forceFallbackPositioning, isEnabled, }: {
|
|
37
|
+
export declare function useAnchorPosition({ anchorRef, popoverRef, placement, forceFallbackPositioning, isEnabled, isOpen, }: {
|
|
38
38
|
/**
|
|
39
39
|
* Element to position relative to.
|
|
40
40
|
*/
|
|
41
41
|
anchorRef: RefObject<HTMLElement | null>;
|
|
42
42
|
/**
|
|
43
43
|
* Element being positioned (typically a popover).
|
|
44
|
+
*
|
|
45
|
+
* The popover host may be unmounted between opens (the `Popover`
|
|
46
|
+
* primitive unmounts after its exit animation). Pass `isOpen` so the
|
|
47
|
+
* positioning effect re-runs against the freshly mounted element on
|
|
48
|
+
* the next open. Without it, the effect's bound listeners and styles
|
|
49
|
+
* would target the previous (detached) element.
|
|
44
50
|
*/
|
|
45
51
|
popoverRef: RefObject<HTMLElement | null>;
|
|
46
52
|
/**
|
|
@@ -75,4 +81,11 @@ export declare function useAnchorPosition({ anchorRef, popoverRef, placement, fo
|
|
|
75
81
|
* Defaults to `true`.
|
|
76
82
|
*/
|
|
77
83
|
isEnabled?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Whether the popover is currently open. Drives a re-run of the
|
|
86
|
+
* positioning effect when the popover host element is unmounted
|
|
87
|
+
* and remounted across open cycles, so listeners and styles are
|
|
88
|
+
* always wired to the live host element.
|
|
89
|
+
*/
|
|
90
|
+
isOpen: boolean;
|
|
78
91
|
}): void;
|
|
@@ -29,16 +29,51 @@ type TUseAnimatedVisibilityArgs = {
|
|
|
29
29
|
};
|
|
30
30
|
type TUseAnimatedVisibilityResult = {
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* Current visibility phase. See `TPhase` for the full lifecycle.
|
|
33
|
+
*
|
|
34
|
+
* `phase !== 'closed'` means the host element is mounted and on
|
|
35
|
+
* screen, including during animated entry, animated exit, and the
|
|
36
|
+
* non-animated close handshake. Use this when you need "the user
|
|
37
|
+
* can still see / interact with the element" semantics (e.g. focus
|
|
38
|
+
* trapping, listeners that must outlive the exit).
|
|
34
39
|
*/
|
|
35
|
-
|
|
40
|
+
phase: TPhase;
|
|
36
41
|
/**
|
|
37
42
|
* Resolved animation preset (with CSS injected), or `null` if
|
|
38
43
|
* animation is disabled.
|
|
39
44
|
*/
|
|
40
45
|
preset: TAnimationPreset | null;
|
|
41
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* Visibility lifecycle phase shared between `useAnimatedVisibility`,
|
|
49
|
+
* `useFocusWrap`, `useInitialFocus`, and the `Popover` / `Dialog`
|
|
50
|
+
* components.
|
|
51
|
+
*
|
|
52
|
+
* - `closed`: host element is unmounted. No DOM presence.
|
|
53
|
+
* - `entering`: host element is mounted and the CSS entry transition
|
|
54
|
+
* is playing. **Only emitted when animation is enabled.** With
|
|
55
|
+
* animation disabled the lifecycle goes directly `closed → open`.
|
|
56
|
+
* - `open`: host element is mounted and settled. No transition is
|
|
57
|
+
* playing.
|
|
58
|
+
* - `exiting`: `isOpen` has flipped to `false` but the host element
|
|
59
|
+
* is still mounted. Emitted while the CSS exit transition is playing
|
|
60
|
+
* (animation enabled) OR while we wait for the browser's
|
|
61
|
+
* `toggle`/`close` event to fire on the still-attached element so
|
|
62
|
+
* focus restoration can run (animation disabled).
|
|
63
|
+
*
|
|
64
|
+
* State machine:
|
|
65
|
+
* ```
|
|
66
|
+
* animation ON: closed → entering → open → exiting → closed
|
|
67
|
+
* animation OFF: closed → open → exiting → closed
|
|
68
|
+
* interrupts: entering → exiting (close mid-entry)
|
|
69
|
+
* exiting → entering (reopen mid-exit, animated)
|
|
70
|
+
* exiting → open (reopen mid-handshake, non-animated)
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* `phase !== 'closed'` is the canonical "host is currently in the DOM
|
|
74
|
+
* and on screen" predicate.
|
|
75
|
+
*/
|
|
76
|
+
export type TPhase = 'closed' | 'entering' | 'open' | 'exiting';
|
|
42
77
|
/**
|
|
43
78
|
* Manages the children mount/unmount lifecycle around CSS exit transitions.
|
|
44
79
|
*
|
|
@@ -55,26 +90,20 @@ type TUseAnimatedVisibilityResult = {
|
|
|
55
90
|
*
|
|
56
91
|
* ## How it works
|
|
57
92
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
93
|
+
* The lifecycle is modeled as a `phase` discriminated union (`closed`,
|
|
94
|
+
* `entering`, `open`, `exiting`). The host element is rendered while
|
|
95
|
+
* `phase !== 'closed'`, so the `exiting` phase keeps the host mounted
|
|
96
|
+
* while the CSS exit transition plays (or, with animation disabled,
|
|
97
|
+
* while we wait for the browser `toggle` / `close` event to fire on the
|
|
98
|
+
* still-attached element):
|
|
61
99
|
*
|
|
62
100
|
* ```
|
|
63
|
-
* isOpen:
|
|
64
|
-
*
|
|
101
|
+
* isOpen: true ──────────────── false
|
|
102
|
+
* phase: open ──────────────── exiting ─── (exit settle) ─── closed
|
|
65
103
|
* ```
|
|
66
104
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* 1. **Animated close** (`willAnimate === true`):
|
|
70
|
-
* `isOpen` goes `false` → `showChildren` stays `true` → the CSS exit
|
|
71
|
-
* transition plays → `transitionend` fires → `showChildren` → `false`
|
|
72
|
-
* → `onExitFinish` fires. A timeout fallback ensures we still unmount
|
|
73
|
-
* if `transitionend` never fires (e.g. browser quirks, zero-duration).
|
|
74
|
-
*
|
|
75
|
-
* 2. **Non-animated close** (`willAnimate === false`):
|
|
76
|
-
* `isOpen` goes `false` → `showChildren` → `false` synchronously during
|
|
77
|
-
* the same render → `onExitFinish` fires in a follow-up effect.
|
|
105
|
+
* Every effect dispatches on the single `phase` value instead of a
|
|
106
|
+
* cross-product of booleans.
|
|
78
107
|
*/
|
|
79
108
|
export declare function useAnimatedVisibility({ isOpen, animate, elementRef, onEnterFinish, onExitFinish, }: TUseAnimatedVisibilityArgs): TUseAnimatedVisibilityResult;
|
|
80
109
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type RefObject } from 'react';
|
|
2
|
+
import { type TPhase } from './use-animated-visibility';
|
|
2
3
|
/**
|
|
3
4
|
* Wraps Tab/Shift+Tab focus within a popover element when its role
|
|
4
5
|
* semantically requires it (dialog, alertdialog).
|
|
@@ -14,7 +15,11 @@ import { type RefObject } from 'react';
|
|
|
14
15
|
* attached when the role requires focus wrapping, and is cleaned
|
|
15
16
|
* up and re-evaluated when the role changes.
|
|
16
17
|
*/
|
|
17
|
-
export declare function useFocusWrap({ elementRef, role, }: {
|
|
18
|
+
export declare function useFocusWrap({ elementRef, role, phase, }: {
|
|
18
19
|
elementRef: RefObject<HTMLElement | null>;
|
|
19
20
|
role: string | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Whether the element is open. Used to attach the Tab listener.
|
|
23
|
+
*/
|
|
24
|
+
phase: TPhase;
|
|
20
25
|
}): void;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type RefObject } from 'react';
|
|
2
|
+
import { type TPhase } from './use-animated-visibility';
|
|
2
3
|
/**
|
|
3
4
|
* Moves focus into a popover element when it opens, based on its ARIA role.
|
|
4
5
|
*
|
|
@@ -12,11 +13,61 @@ import { type RefObject } from 'react';
|
|
|
12
13
|
* Role is taken from the typed `role` prop - this is the consumer's contract
|
|
13
14
|
* and is not subject to drift from a directly-mutated DOM attribute.
|
|
14
15
|
*
|
|
15
|
-
* Always call this hook unconditionally. Focus is
|
|
16
|
-
*
|
|
16
|
+
* Always call this hook unconditionally. Focus is moved on each transition
|
|
17
|
+
* into a visible phase, the moment the host element first becomes visible
|
|
18
|
+
* for that open intent:
|
|
19
|
+
*
|
|
20
|
+
* - Animated path: on `closed → entering`. The host has just been mounted
|
|
21
|
+
* and the layout effect that calls `showPopover()` / `showModal()` has
|
|
22
|
+
* already run, so the element is in the top layer. We focus before the
|
|
23
|
+
* entry transition settles to match the WAI-ARIA APG expectation that
|
|
24
|
+
* focus lands on the new menu / dialog the instant it opens, not after
|
|
25
|
+
* the CSS finishes animating.
|
|
26
|
+
* - Non-animated path: on `closed → open` (the entering phase is skipped).
|
|
27
|
+
* - Reopen mid-exit: on `exiting → entering`. When a user dismisses the
|
|
28
|
+
* popup (Escape, close button, light dismiss) the browser natively
|
|
29
|
+
* restores focus to the trigger as part of `hidePopover()` /
|
|
30
|
+
* `<dialog>.close()`. If the consumer reopens before the exit transition
|
|
31
|
+
* has settled, the phase machine jumps `exiting → entering` without
|
|
32
|
+
* passing through `closed`, so without this branch the popup would be
|
|
33
|
+
* visible again with focus stranded on the trigger — a keyboard usability
|
|
34
|
+
* gap for menu/dialog/listbox roles.
|
|
35
|
+
*
|
|
36
|
+
* Note: there is a rare programmatic path where a consumer flips `isOpen`
|
|
37
|
+
* false → true without any user-initiated close gesture and had moved focus
|
|
38
|
+
* to a non-default target inside the popup. In that scenario this hook
|
|
39
|
+
* re-focuses the role-appropriate default (e.g. first menu item) rather
|
|
40
|
+
* than preserving the consumer-chosen target. This matches the documented
|
|
41
|
+
* `closed → entering` behavior and is considered acceptable given how
|
|
42
|
+
* abstract the scenario is. See `notes/architecture/focus.md`.
|
|
43
|
+
*
|
|
44
|
+
* Pure `entering → exiting → entering` stutters where the popup never
|
|
45
|
+
* reaches `closed` and the consumer toggles in the same frame are still
|
|
46
|
+
* collapsed: the `prevPhase` ref guard rejects any transition whose
|
|
47
|
+
* previous phase was not `closed` or `exiting`.
|
|
48
|
+
*
|
|
49
|
+
* Tradeoff vs the previous `requestAnimationFrame` based approach: we no
|
|
50
|
+
* longer wait one paint before focusing. The element is already attached
|
|
51
|
+
* and in the top layer when this effect runs (the show / hide layout
|
|
52
|
+
* effect runs first), so screen-reader announcement and visible focus
|
|
53
|
+
* land together. If a regression appears that needs the extra paint
|
|
54
|
+
* (e.g. browsers that require a layout pass before `focus()` works on a
|
|
55
|
+
* freshly promoted top-layer element), wrap the `focus()` call in a
|
|
56
|
+
* single `requestAnimationFrame` here and re-add a same-element ref
|
|
57
|
+
* check inside the RAF callback.
|
|
17
58
|
*/
|
|
18
|
-
export declare function useInitialFocus({ elementRef,
|
|
59
|
+
export declare function useInitialFocus({ elementRef, phase, role, }: {
|
|
19
60
|
elementRef: RefObject<HTMLElement | null>;
|
|
20
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Current visibility phase from `useAnimatedVisibility`. Initial
|
|
63
|
+
* focus is moved on the transition out of `'closed'`:
|
|
64
|
+
*
|
|
65
|
+
* - Animated path: fires on `closed → entering`, immediately after
|
|
66
|
+
* the host element mounts and `showPopover()` / `showModal()` has
|
|
67
|
+
* run in the preceding layout effect.
|
|
68
|
+
* - Non-animated path: fires on `closed → open` (entering phase is
|
|
69
|
+
* skipped entirely when animation is disabled).
|
|
70
|
+
*/
|
|
71
|
+
phase: TPhase;
|
|
21
72
|
role: string | undefined;
|
|
22
73
|
}): void;
|
|
@@ -13,8 +13,9 @@ import type { TWidthFromAnchorMode } from '../popover/types';
|
|
|
13
13
|
*
|
|
14
14
|
* Designed to be composed alongside `useAnchorPosition` for top-layer popovers.
|
|
15
15
|
*/
|
|
16
|
-
export declare function useWidthFromAnchor({ mode, popoverRef, anchorRef, }: {
|
|
16
|
+
export declare function useWidthFromAnchor({ mode, popoverRef, anchorRef, isOpen, }: {
|
|
17
17
|
mode: TWidthFromAnchorMode;
|
|
18
18
|
popoverRef: RefObject<HTMLElement | null>;
|
|
19
|
-
anchorRef: RefObject<HTMLElement | null
|
|
19
|
+
anchorRef: RefObject<HTMLElement | null>;
|
|
20
|
+
isOpen: boolean;
|
|
20
21
|
}): void;
|
|
@@ -65,8 +65,19 @@ type TPopoverBaseProps = {
|
|
|
65
65
|
* animation plays (via `allow-discrete`) before the popover becomes logically closed.
|
|
66
66
|
* Otherwise hides instantly.
|
|
67
67
|
*
|
|
68
|
-
* The
|
|
69
|
-
*
|
|
68
|
+
* The consumer does not conditionally render the `Popover` — visibility is driven
|
|
69
|
+
* by this prop.
|
|
70
|
+
*
|
|
71
|
+
* **Lifecycle observable to consumers:**
|
|
72
|
+
*
|
|
73
|
+
* - The host element is in the DOM only while the popover is open or its exit
|
|
74
|
+
* animation is playing. After exit completes the element is unmounted so it
|
|
75
|
+
* does not leave an empty `role` / `popover` element in the accessibility tree.
|
|
76
|
+
* The exact unmount timing is private and may change.
|
|
77
|
+
* - The `id` (supplied or generated via `usePopoverId()`) is stable across opens.
|
|
78
|
+
* - The `ref` is populated only while the host element is rendered. Consumers
|
|
79
|
+
* that read from the ref outside of `onOpenChange` / `onEnterFinish` should
|
|
80
|
+
* gate the read on `isOpen` being `true`.
|
|
70
81
|
*
|
|
71
82
|
* **Important:** For `mode="auto"` popovers, the browser can dismiss the popover
|
|
72
83
|
* via light dismiss (Escape, click outside) independently of this prop. When that
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlaskit/top-layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Low-level layering primitives based on popover and dialog",
|
|
5
5
|
"homepage": "https://atlassian.design/components/top-layer",
|
|
6
6
|
"repository": "https://bitbucket.org/atlassian/atlassian-frontend-mirror",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@atlaskit/css": "^0.19.0",
|
|
39
39
|
"@atlaskit/ds-lib": "^7.0.0",
|
|
40
40
|
"@atlaskit/layering": "^3.8.0",
|
|
41
|
-
"@atlaskit/tokens": "^13.
|
|
41
|
+
"@atlaskit/tokens": "^13.4.0",
|
|
42
42
|
"@babel/runtime": "^7.0.0",
|
|
43
43
|
"@compiled/react": "^0.20.0",
|
|
44
44
|
"bind-event-listener": "^3.0.0",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@af/visual-regression": "workspace:^",
|
|
54
54
|
"@atlaskit/button": "^23.11.0",
|
|
55
55
|
"@atlaskit/heading": "^5.4.0",
|
|
56
|
-
"@atlaskit/lozenge": "^13.
|
|
56
|
+
"@atlaskit/lozenge": "^13.9.0",
|
|
57
57
|
"@atlaskit/primitives": "^19.0.0",
|
|
58
58
|
"@atlaskit/ssr": "workspace:^",
|
|
59
59
|
"@atlaskit/textfield": "^8.3.0",
|