@fluentui/react-aria 9.10.5 → 9.11.1

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 (29) hide show
  1. package/CHANGELOG.md +30 -2
  2. package/dist/index.d.ts +12 -0
  3. package/lib/AriaLiveAnnouncer/useAriaLiveAnnouncer.js +12 -114
  4. package/lib/AriaLiveAnnouncer/useAriaLiveAnnouncer.js.map +1 -1
  5. package/lib/AriaLiveAnnouncer/useAriaNotifyAnnounce.js +26 -0
  6. package/lib/AriaLiveAnnouncer/useAriaNotifyAnnounce.js.map +1 -0
  7. package/lib/AriaLiveAnnouncer/useDomAnnounce.js +121 -0
  8. package/lib/AriaLiveAnnouncer/useDomAnnounce.js.map +1 -0
  9. package/lib/activedescendant/ActiveDescendantContext.js +1 -0
  10. package/lib/activedescendant/ActiveDescendantContext.js.map +1 -1
  11. package/lib/activedescendant/scrollIntoView.js +44 -9
  12. package/lib/activedescendant/scrollIntoView.js.map +1 -1
  13. package/lib/activedescendant/types.js.map +1 -1
  14. package/lib/activedescendant/useActiveDescendant.js +27 -3
  15. package/lib/activedescendant/useActiveDescendant.js.map +1 -1
  16. package/lib/index.js.map +1 -1
  17. package/lib-commonjs/AriaLiveAnnouncer/useAriaLiveAnnouncer.js +12 -114
  18. package/lib-commonjs/AriaLiveAnnouncer/useAriaLiveAnnouncer.js.map +1 -1
  19. package/lib-commonjs/AriaLiveAnnouncer/useAriaNotifyAnnounce.js +37 -0
  20. package/lib-commonjs/AriaLiveAnnouncer/useAriaNotifyAnnounce.js.map +1 -0
  21. package/lib-commonjs/AriaLiveAnnouncer/useDomAnnounce.js +132 -0
  22. package/lib-commonjs/AriaLiveAnnouncer/useDomAnnounce.js.map +1 -0
  23. package/lib-commonjs/activedescendant/ActiveDescendantContext.js +1 -0
  24. package/lib-commonjs/activedescendant/ActiveDescendantContext.js.map +1 -1
  25. package/lib-commonjs/activedescendant/scrollIntoView.js +44 -9
  26. package/lib-commonjs/activedescendant/scrollIntoView.js.map +1 -1
  27. package/lib-commonjs/activedescendant/useActiveDescendant.js +38 -6
  28. package/lib-commonjs/activedescendant/useActiveDescendant.js.map +1 -1
  29. package/package.json +5 -5
package/CHANGELOG.md CHANGED
@@ -1,12 +1,40 @@
1
1
  # Change Log - @fluentui/react-aria
2
2
 
3
- This log was last generated on Tue, 23 Apr 2024 08:12:12 GMT and should not be manually modified.
3
+ This log was last generated on Mon, 06 May 2024 12:48:52 GMT and should not be manually modified.
4
4
 
5
5
  <!-- Start content -->
6
6
 
7
+ ## [9.11.1](https://github.com/microsoft/fluentui/tree/@fluentui/react-aria_v9.11.1)
8
+
9
+ Mon, 06 May 2024 12:48:52 GMT
10
+ [Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/react-aria_v9.11.0..@fluentui/react-aria_v9.11.1)
11
+
12
+ ### Patches
13
+
14
+ - feat: use ariaNotify in AriaLiveAnnouncer when available ([PR #31251](https://github.com/microsoft/fluentui/pull/31251) by sarah.higley@microsoft.com)
15
+ - Bump @fluentui/react-shared-contexts to v9.18.0 ([PR #31271](https://github.com/microsoft/fluentui/pull/31271) by beachball)
16
+ - Bump @fluentui/react-jsx-runtime to v9.0.37 ([PR #31271](https://github.com/microsoft/fluentui/pull/31271) by beachball)
17
+ - Bump @fluentui/react-tabster to v9.21.1 ([PR #31271](https://github.com/microsoft/fluentui/pull/31271) by beachball)
18
+ - Bump @fluentui/react-utilities to v9.18.8 ([PR #31271](https://github.com/microsoft/fluentui/pull/31271) by beachball)
19
+
20
+ ## [9.11.0](https://github.com/microsoft/fluentui/tree/@fluentui/react-aria_v9.11.0)
21
+
22
+ Thu, 02 May 2024 11:36:29 GMT
23
+ [Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/react-aria_v9.10.5..@fluentui/react-aria_v9.11.0)
24
+
25
+ ### Minor changes
26
+
27
+ - Adding function to focus last active descendant if it exists ([PR #31140](https://github.com/microsoft/fluentui/pull/31140) by stevenco@microsoft.com)
28
+ - ActiveDescendantChangeEvent for tracking active item ([PR #31149](https://github.com/microsoft/fluentui/pull/31149) by jurokapsiar@gmail.com)
29
+ - Bump @fluentui/react-tabster to v9.21.0 ([PR #31231](https://github.com/microsoft/fluentui/pull/31231) by beachball)
30
+
31
+ ### Patches
32
+
33
+ - Updating ActiveDescendent scrollIntoView logic to work with ancester scroll containers and scroll margin styles ([PR #31158](https://github.com/microsoft/fluentui/pull/31158) by stevenco@microsoft.com)
34
+
7
35
  ## [9.10.5](https://github.com/microsoft/fluentui/tree/@fluentui/react-aria_v9.10.5)
8
36
 
9
- Tue, 23 Apr 2024 08:12:12 GMT
37
+ Tue, 23 Apr 2024 08:17:49 GMT
10
38
  [Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/react-aria_v9.10.4..@fluentui/react-aria_v9.10.5)
11
39
 
12
40
  ### Patches
package/dist/index.d.ts CHANGED
@@ -10,6 +10,13 @@ import type { UnionToIntersection } from '@fluentui/react-utilities';
10
10
  */
11
11
  export declare const ACTIVEDESCENDANT_FOCUSVISIBLE_ATTRIBUTE = "data-activedescendant-focusvisible";
12
12
 
13
+ export declare type ActiveDescendantChangeEvent = CustomEvent<ActiveDescendantChangeEventDetail>;
14
+
15
+ declare interface ActiveDescendantChangeEventDetail {
16
+ id: string;
17
+ previousId: string | null;
18
+ }
19
+
13
20
  export declare const ActiveDescendantContextProvider: React_2.Provider<ActiveDescendantContextValue | undefined>;
14
21
 
15
22
  export declare type ActiveDescendantContextValue = {
@@ -25,6 +32,11 @@ export declare interface ActiveDescendantImperativeRef {
25
32
  blur: () => void;
26
33
  active: () => string | undefined;
27
34
  focus: (id: string) => void;
35
+ /**
36
+ * Focuses the last active descendant, if it still exists
37
+ * @returns true if the last active descendant was focused
38
+ */
39
+ focusLastActive: () => boolean | undefined;
28
40
  hideAttributes: () => void;
29
41
  showAttributes: () => void;
30
42
  }
@@ -1,121 +1,19 @@
1
- import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
2
- import { createPriorityQueue, useTimeout } from '@fluentui/react-utilities';
3
1
  import * as React from 'react';
4
- /** The duration the message needs to be in present in DOM for screen readers to register a change and announce */ const MESSAGE_DURATION = 500;
5
- const VISUALLY_HIDDEN_STYLES = {
6
- clip: 'rect(0px, 0px, 0px, 0px)',
7
- height: '1px',
8
- margin: '-1px',
9
- width: '1px',
10
- position: 'absolute',
11
- overflow: 'hidden',
12
- textWrap: 'nowrap'
13
- };
2
+ import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
3
+ import { useDomAnnounce_unstable } from './useDomAnnounce';
4
+ import { useAriaNotifyAnnounce_unstable } from './useAriaNotifyAnnounce';
14
5
  export const useAriaLiveAnnouncer_unstable = (props)=>{
15
6
  const { targetDocument } = useFluent();
16
- const timeoutRef = React.useRef(undefined);
17
- const [setAnnounceTimeout, clearAnnounceTimeout] = useTimeout();
18
- const elementRef = React.useRef(null);
19
- const order = React.useRef(0);
20
- // investigate alert implementation later
21
- // const [alertList, setAlertList] = React.useState<string[]>([]);
22
- const batchMessages = React.useRef([]);
23
- const [messageQueue] = React.useState(()=>createPriorityQueue((a, b)=>{
24
- if (a.priority !== b.priority) {
25
- return b.priority - a.priority;
26
- }
27
- return a.createdAt - b.createdAt;
28
- }));
29
- const queueMessage = React.useCallback(()=>{
30
- if (timeoutRef.current || !elementRef.current) {
31
- return;
32
- }
33
- const runCycle = ()=>{
34
- if (!elementRef.current) {
35
- return;
36
- }
37
- if (targetDocument && messageQueue.peek()) {
38
- // need a wrapping element for Narrator/Edge, which currently does not pick up text-only live region changes
39
- // consistently
40
- // if this is fixed, we can set textContent to the string directly
41
- const wrappingEl = targetDocument.createElement('span');
42
- wrappingEl.innerText = messageQueue.all().filter((msg)=>msg.message.trim().length > 0).reduce((prevText, currMsg)=>prevText + currMsg.message + '. ', '');
43
- elementRef.current.innerText = '';
44
- elementRef.current.appendChild(wrappingEl);
45
- messageQueue.clear();
46
- batchMessages.current = [];
47
- // begin new cycle to clear (or update) messages
48
- timeoutRef.current = setAnnounceTimeout(()=>{
49
- runCycle();
50
- }, MESSAGE_DURATION);
51
- } else {
52
- elementRef.current.textContent = '';
53
- clearAnnounceTimeout();
54
- timeoutRef.current = undefined;
55
- }
56
- };
57
- runCycle();
58
- }, [
59
- clearAnnounceTimeout,
60
- messageQueue,
61
- setAnnounceTimeout,
62
- targetDocument
63
- ]);
64
- const announce = React.useMemo(()=>(message, options = {})=>{
65
- const { alert = false, priority = 0, batchId } = options;
66
- // check if message is an alert
67
- if (alert) {
68
- // TODO: alert implementation
69
- // setAlertList([...alertList, message]);
70
- }
71
- const liveMessage = {
72
- message,
73
- createdAt: order.current++,
74
- priority,
75
- batchId
76
- };
77
- // check if batchId exists
78
- if (batchId) {
79
- // update associated msg if it does
80
- const batchMessage = batchMessages.current.find((msg)=>msg.batchId === batchId);
81
- if (batchMessage) {
82
- // replace existing message in queue
83
- messageQueue.remove(batchMessage.message);
84
- // update list of existing batchIds w/ most recent message
85
- batchMessage.message = liveMessage;
86
- } else {
87
- // update list of existing batchIds, add new if doesn't already exist
88
- batchMessages.current = [
89
- ...batchMessages.current,
90
- {
91
- batchId,
92
- message: liveMessage
93
- }
94
- ];
95
- }
96
- }
97
- // add new message
98
- messageQueue.enqueue(liveMessage);
99
- queueMessage();
100
- }, [
101
- messageQueue,
102
- queueMessage
103
- ]);
104
- React.useEffect(()=>{
105
- if (!targetDocument) {
106
- return;
107
- }
108
- const element = targetDocument.createElement('div');
109
- element.setAttribute('aria-live', 'assertive');
110
- Object.assign(element.style, VISUALLY_HIDDEN_STYLES);
111
- targetDocument.body.append(element);
112
- elementRef.current = element;
113
- return ()=>{
114
- element.remove();
115
- elementRef.current = null;
116
- };
7
+ const domAnnounce = useDomAnnounce_unstable();
8
+ const ariaNotifyAnnounce = useAriaNotifyAnnounce_unstable();
9
+ const announce = React.useMemo(()=>{
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ const supportsAriaNotify = typeof (targetDocument === null || targetDocument === void 0 ? void 0 : targetDocument.ariaNotify) === 'function';
12
+ return supportsAriaNotify ? ariaNotifyAnnounce : domAnnounce;
117
13
  }, [
118
- targetDocument
14
+ targetDocument,
15
+ ariaNotifyAnnounce,
16
+ domAnnounce
119
17
  ]);
120
18
  return {
121
19
  announce,
@@ -1 +1 @@
1
- {"version":3,"sources":["useAriaLiveAnnouncer.ts"],"sourcesContent":["import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';\nimport { createPriorityQueue, useTimeout } from '@fluentui/react-utilities';\nimport * as React from 'react';\n\nimport type {\n AriaLiveAnnounceFn,\n AriaLiveAnnouncerState,\n AriaLiveAnnouncerProps,\n AriaLiveMessage,\n} from './AriaLiveAnnouncer.types';\n\n/** The duration the message needs to be in present in DOM for screen readers to register a change and announce */\nconst MESSAGE_DURATION = 500;\n\nconst VISUALLY_HIDDEN_STYLES = {\n clip: 'rect(0px, 0px, 0px, 0px)',\n height: '1px',\n margin: '-1px',\n width: '1px',\n position: 'absolute',\n overflow: 'hidden',\n textWrap: 'nowrap',\n};\n\nexport const useAriaLiveAnnouncer_unstable = (props: AriaLiveAnnouncerProps): AriaLiveAnnouncerState => {\n const { targetDocument } = useFluent();\n\n const timeoutRef = React.useRef<number | undefined>(undefined);\n const [setAnnounceTimeout, clearAnnounceTimeout] = useTimeout();\n\n const elementRef = React.useRef<HTMLDivElement | null>(null);\n\n const order = React.useRef(0);\n\n // investigate alert implementation later\n // const [alertList, setAlertList] = React.useState<string[]>([]);\n\n const batchMessages = React.useRef<{ batchId: string; message: AriaLiveMessage }[]>([]);\n\n const [messageQueue] = React.useState(() =>\n createPriorityQueue<AriaLiveMessage>((a, b) => {\n if (a.priority !== b.priority) {\n return b.priority - a.priority;\n }\n\n return a.createdAt - b.createdAt;\n }),\n );\n\n const queueMessage = React.useCallback(() => {\n if (timeoutRef.current || !elementRef.current) {\n return;\n }\n\n const runCycle = () => {\n if (!elementRef.current) {\n return;\n }\n\n if (targetDocument && messageQueue.peek()) {\n // need a wrapping element for Narrator/Edge, which currently does not pick up text-only live region changes\n // consistently\n // if this is fixed, we can set textContent to the string directly\n\n const wrappingEl = targetDocument.createElement('span');\n\n wrappingEl.innerText = messageQueue\n .all()\n .filter(msg => msg.message.trim().length > 0)\n .reduce((prevText, currMsg) => prevText + currMsg.message + '. ', '');\n\n elementRef.current.innerText = '';\n elementRef.current.appendChild(wrappingEl);\n\n messageQueue.clear();\n batchMessages.current = [];\n\n // begin new cycle to clear (or update) messages\n timeoutRef.current = setAnnounceTimeout(() => {\n runCycle();\n }, MESSAGE_DURATION);\n } else {\n elementRef.current.textContent = '';\n clearAnnounceTimeout();\n\n timeoutRef.current = undefined;\n }\n };\n\n runCycle();\n }, [clearAnnounceTimeout, messageQueue, setAnnounceTimeout, targetDocument]);\n\n const announce: AriaLiveAnnounceFn = React.useMemo(\n () =>\n (message, options = {}) => {\n const { alert = false, priority = 0, batchId } = options;\n\n // check if message is an alert\n if (alert) {\n // TODO: alert implementation\n // setAlertList([...alertList, message]);\n }\n\n const liveMessage: AriaLiveMessage = {\n message,\n createdAt: order.current++,\n priority,\n batchId,\n };\n\n // check if batchId exists\n if (batchId) {\n // update associated msg if it does\n const batchMessage = batchMessages.current.find(msg => msg.batchId === batchId);\n\n if (batchMessage) {\n // replace existing message in queue\n messageQueue.remove(batchMessage.message);\n\n // update list of existing batchIds w/ most recent message\n batchMessage.message = liveMessage;\n } else {\n // update list of existing batchIds, add new if doesn't already exist\n batchMessages.current = [...batchMessages.current, { batchId, message: liveMessage }];\n }\n }\n\n // add new message\n messageQueue.enqueue(liveMessage);\n queueMessage();\n },\n [messageQueue, queueMessage],\n );\n\n React.useEffect(() => {\n if (!targetDocument) {\n return;\n }\n\n const element = targetDocument.createElement('div');\n element.setAttribute('aria-live', 'assertive');\n\n Object.assign(element.style, VISUALLY_HIDDEN_STYLES);\n targetDocument.body.append(element);\n\n elementRef.current = element;\n\n return () => {\n element.remove();\n elementRef.current = null;\n };\n }, [targetDocument]);\n\n return {\n announce,\n children: props.children,\n };\n};\n"],"names":["useFluent_unstable","useFluent","createPriorityQueue","useTimeout","React","MESSAGE_DURATION","VISUALLY_HIDDEN_STYLES","clip","height","margin","width","position","overflow","textWrap","useAriaLiveAnnouncer_unstable","props","targetDocument","timeoutRef","useRef","undefined","setAnnounceTimeout","clearAnnounceTimeout","elementRef","order","batchMessages","messageQueue","useState","a","b","priority","createdAt","queueMessage","useCallback","current","runCycle","peek","wrappingEl","createElement","innerText","all","filter","msg","message","trim","length","reduce","prevText","currMsg","appendChild","clear","textContent","announce","useMemo","options","alert","batchId","liveMessage","batchMessage","find","remove","enqueue","useEffect","element","setAttribute","Object","assign","style","body","append","children"],"mappings":"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"}
1
+ {"version":3,"sources":["useAriaLiveAnnouncer.ts"],"sourcesContent":["import * as React from 'react';\nimport { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';\nimport { useDomAnnounce_unstable } from './useDomAnnounce';\nimport { useAriaNotifyAnnounce_unstable } from './useAriaNotifyAnnounce';\n\nimport type { AriaLiveAnnouncerState, AriaLiveAnnouncerProps } from './AriaLiveAnnouncer.types';\n\nexport const useAriaLiveAnnouncer_unstable = (props: AriaLiveAnnouncerProps): AriaLiveAnnouncerState => {\n const { targetDocument } = useFluent();\n const domAnnounce = useDomAnnounce_unstable();\n const ariaNotifyAnnounce = useAriaNotifyAnnounce_unstable();\n\n const announce = React.useMemo(() => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const supportsAriaNotify = typeof (targetDocument as any)?.ariaNotify === 'function';\n return supportsAriaNotify ? ariaNotifyAnnounce : domAnnounce;\n }, [targetDocument, ariaNotifyAnnounce, domAnnounce]);\n\n return {\n announce,\n children: props.children,\n };\n};\n"],"names":["React","useFluent_unstable","useFluent","useDomAnnounce_unstable","useAriaNotifyAnnounce_unstable","useAriaLiveAnnouncer_unstable","props","targetDocument","domAnnounce","ariaNotifyAnnounce","announce","useMemo","supportsAriaNotify","ariaNotify","children"],"mappings":"AAAA,YAAYA,WAAW,QAAQ;AAC/B,SAASC,sBAAsBC,SAAS,QAAQ,kCAAkC;AAClF,SAASC,uBAAuB,QAAQ,mBAAmB;AAC3D,SAASC,8BAA8B,QAAQ,0BAA0B;AAIzE,OAAO,MAAMC,gCAAgC,CAACC;IAC5C,MAAM,EAAEC,cAAc,EAAE,GAAGL;IAC3B,MAAMM,cAAcL;IACpB,MAAMM,qBAAqBL;IAE3B,MAAMM,WAAWV,MAAMW,OAAO,CAAC;QAC7B,8DAA8D;QAC9D,MAAMC,qBAAqB,QAAQL,2BAAAA,qCAAD,AAACA,eAAwBM,UAAU,MAAK;QAC1E,OAAOD,qBAAqBH,qBAAqBD;IACnD,GAAG;QAACD;QAAgBE;QAAoBD;KAAY;IAEpD,OAAO;QACLE;QACAI,UAAUR,MAAMQ,QAAQ;IAC1B;AACF,EAAE"}
@@ -0,0 +1,26 @@
1
+ import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
2
+ import * as React from 'react';
3
+ /* INTERNAL: implementation of the announcer using the ariaNotify API */ export const useAriaNotifyAnnounce_unstable = ()=>{
4
+ const { targetDocument } = useFluent();
5
+ const announce = React.useCallback((message, options = {})=>{
6
+ if (!targetDocument) {
7
+ return;
8
+ }
9
+ const { alert = false, polite, batchId } = options;
10
+ // default priority to 0 if polite, 2 if alert, and 1 by default
11
+ // used to set both ariaNotify's priority and interrupt
12
+ const defaultPriority = polite ? 0 : alert ? 2 : 1;
13
+ var _options_priority;
14
+ const priority = (_options_priority = options.priority) !== null && _options_priority !== void 0 ? _options_priority : defaultPriority;
15
+ // map fluent announce options to ariaNotify options
16
+ const ariaNotifyOptions = {
17
+ notificationID: batchId,
18
+ priority: priority > 1 ? 'important' : 'none',
19
+ interrupt: batchId ? priority > 0 ? 'all' : 'pending' : 'none'
20
+ };
21
+ targetDocument.ariaNotify(message, ariaNotifyOptions);
22
+ }, [
23
+ targetDocument
24
+ ]);
25
+ return announce;
26
+ };
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["useAriaNotifyAnnounce.ts"],"sourcesContent":["import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';\nimport type { AnnounceOptions } from '@fluentui/react-shared-contexts';\nimport * as React from 'react';\n\nimport type { AriaLiveAnnounceFn } from './AriaLiveAnnouncer.types';\n\ntype AriaNotifyOptions = {\n notificationID?: string;\n priority?: 'none' | 'important';\n interrupt?: 'all' | 'pending' | 'none';\n};\n\ntype DocumentWithAriaNotify = Document & {\n ariaNotify: (message: string, options: AriaNotifyOptions) => void;\n};\n\n/* INTERNAL: implementation of the announcer using the ariaNotify API */\nexport const useAriaNotifyAnnounce_unstable = (): AriaLiveAnnounceFn => {\n const { targetDocument } = useFluent();\n\n const announce: AriaLiveAnnounceFn = React.useCallback(\n (message: string, options: AnnounceOptions = {}) => {\n if (!targetDocument) {\n return;\n }\n\n const { alert = false, polite, batchId } = options;\n\n // default priority to 0 if polite, 2 if alert, and 1 by default\n // used to set both ariaNotify's priority and interrupt\n const defaultPriority = polite ? 0 : alert ? 2 : 1;\n const priority = options.priority ?? defaultPriority;\n\n // map fluent announce options to ariaNotify options\n const ariaNotifyOptions: AriaNotifyOptions = {\n notificationID: batchId,\n priority: priority > 1 ? 'important' : 'none',\n interrupt: batchId ? (priority > 0 ? 'all' : 'pending') : 'none',\n };\n\n (targetDocument as DocumentWithAriaNotify).ariaNotify(message, ariaNotifyOptions);\n },\n [targetDocument],\n );\n\n return announce;\n};\n"],"names":["useFluent_unstable","useFluent","React","useAriaNotifyAnnounce_unstable","targetDocument","announce","useCallback","message","options","alert","polite","batchId","defaultPriority","priority","ariaNotifyOptions","notificationID","interrupt","ariaNotify"],"mappings":"AAAA,SAASA,sBAAsBC,SAAS,QAAQ,kCAAkC;AAElF,YAAYC,WAAW,QAAQ;AAc/B,sEAAsE,GACtE,OAAO,MAAMC,iCAAiC;IAC5C,MAAM,EAAEC,cAAc,EAAE,GAAGH;IAE3B,MAAMI,WAA+BH,MAAMI,WAAW,CACpD,CAACC,SAAiBC,UAA2B,CAAC,CAAC;QAC7C,IAAI,CAACJ,gBAAgB;YACnB;QACF;QAEA,MAAM,EAAEK,QAAQ,KAAK,EAAEC,MAAM,EAAEC,OAAO,EAAE,GAAGH;QAE3C,gEAAgE;QAChE,uDAAuD;QACvD,MAAMI,kBAAkBF,SAAS,IAAID,QAAQ,IAAI;YAChCD;QAAjB,MAAMK,WAAWL,CAAAA,oBAAAA,QAAQK,QAAQ,cAAhBL,+BAAAA,oBAAoBI;QAErC,oDAAoD;QACpD,MAAME,oBAAuC;YAC3CC,gBAAgBJ;YAChBE,UAAUA,WAAW,IAAI,cAAc;YACvCG,WAAWL,UAAWE,WAAW,IAAI,QAAQ,YAAa;QAC5D;QAECT,eAA0Ca,UAAU,CAACV,SAASO;IACjE,GACA;QAACV;KAAe;IAGlB,OAAOC;AACT,EAAE"}
@@ -0,0 +1,121 @@
1
+ import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';
2
+ import { createPriorityQueue, useTimeout } from '@fluentui/react-utilities';
3
+ import * as React from 'react';
4
+ /** The duration the message needs to be in present in DOM for screen readers to register a change and announce */ const MESSAGE_DURATION = 500;
5
+ const VISUALLY_HIDDEN_STYLES = {
6
+ clip: 'rect(0px, 0px, 0px, 0px)',
7
+ height: '1px',
8
+ margin: '-1px',
9
+ width: '1px',
10
+ position: 'absolute',
11
+ overflow: 'hidden',
12
+ textWrap: 'nowrap'
13
+ };
14
+ /* INTERNAL: implementation of the announcer using a live region element */ export const useDomAnnounce_unstable = ()=>{
15
+ const { targetDocument } = useFluent();
16
+ const timeoutRef = React.useRef(undefined);
17
+ const [setAnnounceTimeout, clearAnnounceTimeout] = useTimeout();
18
+ const elementRef = React.useRef(null);
19
+ const order = React.useRef(0);
20
+ // investigate alert implementation later
21
+ // const [alertList, setAlertList] = React.useState<string[]>([]);
22
+ const batchMessages = React.useRef([]);
23
+ const [messageQueue] = React.useState(()=>createPriorityQueue((a, b)=>{
24
+ if (a.priority !== b.priority) {
25
+ return b.priority - a.priority;
26
+ }
27
+ return a.createdAt - b.createdAt;
28
+ }));
29
+ const queueMessage = React.useCallback(()=>{
30
+ if (timeoutRef.current || !elementRef.current) {
31
+ return;
32
+ }
33
+ const runCycle = ()=>{
34
+ if (!elementRef.current) {
35
+ return;
36
+ }
37
+ if (targetDocument && messageQueue.peek()) {
38
+ // need a wrapping element for Narrator/Edge, which currently does not pick up text-only live region changes
39
+ // consistently
40
+ // if this is fixed, we can set textContent to the string directly
41
+ const wrappingEl = targetDocument.createElement('span');
42
+ wrappingEl.innerText = messageQueue.all().filter((msg)=>msg.message.trim().length > 0).reduce((prevText, currMsg)=>prevText + currMsg.message + '. ', '');
43
+ elementRef.current.innerText = '';
44
+ elementRef.current.appendChild(wrappingEl);
45
+ messageQueue.clear();
46
+ batchMessages.current = [];
47
+ // begin new cycle to clear (or update) messages
48
+ timeoutRef.current = setAnnounceTimeout(()=>{
49
+ runCycle();
50
+ }, MESSAGE_DURATION);
51
+ } else {
52
+ elementRef.current.textContent = '';
53
+ clearAnnounceTimeout();
54
+ timeoutRef.current = undefined;
55
+ }
56
+ };
57
+ runCycle();
58
+ }, [
59
+ clearAnnounceTimeout,
60
+ messageQueue,
61
+ setAnnounceTimeout,
62
+ targetDocument
63
+ ]);
64
+ const announce = React.useCallback((message, options = {})=>{
65
+ const { alert = false, priority = 0, batchId } = options;
66
+ // check if message is an alert
67
+ if (alert) {
68
+ // TODO: alert implementation
69
+ // setAlertList([...alertList, message]);
70
+ }
71
+ const liveMessage = {
72
+ message,
73
+ createdAt: order.current++,
74
+ priority,
75
+ batchId
76
+ };
77
+ // check if batchId exists
78
+ if (batchId) {
79
+ // update associated msg if it does
80
+ const batchMessage = batchMessages.current.find((msg)=>msg.batchId === batchId);
81
+ if (batchMessage) {
82
+ // replace existing message in queue
83
+ messageQueue.remove(batchMessage.message);
84
+ // update list of existing batchIds w/ most recent message
85
+ batchMessage.message = liveMessage;
86
+ } else {
87
+ // update list of existing batchIds, add new if doesn't already exist
88
+ batchMessages.current = [
89
+ ...batchMessages.current,
90
+ {
91
+ batchId,
92
+ message: liveMessage
93
+ }
94
+ ];
95
+ }
96
+ }
97
+ // add new message
98
+ messageQueue.enqueue(liveMessage);
99
+ queueMessage();
100
+ }, [
101
+ messageQueue,
102
+ queueMessage
103
+ ]);
104
+ React.useEffect(()=>{
105
+ if (!targetDocument) {
106
+ return;
107
+ }
108
+ const element = targetDocument.createElement('div');
109
+ element.setAttribute('aria-live', 'assertive');
110
+ Object.assign(element.style, VISUALLY_HIDDEN_STYLES);
111
+ targetDocument.body.append(element);
112
+ elementRef.current = element;
113
+ return ()=>{
114
+ element.remove();
115
+ elementRef.current = null;
116
+ };
117
+ }, [
118
+ targetDocument
119
+ ]);
120
+ return announce;
121
+ };
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["useDomAnnounce.ts"],"sourcesContent":["import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts';\nimport type { AnnounceOptions } from '@fluentui/react-shared-contexts';\nimport { createPriorityQueue, useTimeout } from '@fluentui/react-utilities';\nimport * as React from 'react';\n\nimport type { AriaLiveAnnounceFn, AriaLiveMessage } from './AriaLiveAnnouncer.types';\n\n/** The duration the message needs to be in present in DOM for screen readers to register a change and announce */\nconst MESSAGE_DURATION = 500;\n\nconst VISUALLY_HIDDEN_STYLES = {\n clip: 'rect(0px, 0px, 0px, 0px)',\n height: '1px',\n margin: '-1px',\n width: '1px',\n position: 'absolute',\n overflow: 'hidden',\n textWrap: 'nowrap',\n};\n\n/* INTERNAL: implementation of the announcer using a live region element */\nexport const useDomAnnounce_unstable = (): AriaLiveAnnounceFn => {\n const { targetDocument } = useFluent();\n\n const timeoutRef = React.useRef<number | undefined>(undefined);\n const [setAnnounceTimeout, clearAnnounceTimeout] = useTimeout();\n\n const elementRef = React.useRef<HTMLDivElement | null>(null);\n\n const order = React.useRef(0);\n\n // investigate alert implementation later\n // const [alertList, setAlertList] = React.useState<string[]>([]);\n\n const batchMessages = React.useRef<{ batchId: string; message: AriaLiveMessage }[]>([]);\n\n const [messageQueue] = React.useState(() =>\n createPriorityQueue<AriaLiveMessage>((a, b) => {\n if (a.priority !== b.priority) {\n return b.priority - a.priority;\n }\n\n return a.createdAt - b.createdAt;\n }),\n );\n\n const queueMessage = React.useCallback(() => {\n if (timeoutRef.current || !elementRef.current) {\n return;\n }\n\n const runCycle = () => {\n if (!elementRef.current) {\n return;\n }\n\n if (targetDocument && messageQueue.peek()) {\n // need a wrapping element for Narrator/Edge, which currently does not pick up text-only live region changes\n // consistently\n // if this is fixed, we can set textContent to the string directly\n\n const wrappingEl = targetDocument.createElement('span');\n\n wrappingEl.innerText = messageQueue\n .all()\n .filter(msg => msg.message.trim().length > 0)\n .reduce((prevText, currMsg) => prevText + currMsg.message + '. ', '');\n\n elementRef.current.innerText = '';\n elementRef.current.appendChild(wrappingEl);\n\n messageQueue.clear();\n batchMessages.current = [];\n\n // begin new cycle to clear (or update) messages\n timeoutRef.current = setAnnounceTimeout(() => {\n runCycle();\n }, MESSAGE_DURATION);\n } else {\n elementRef.current.textContent = '';\n clearAnnounceTimeout();\n\n timeoutRef.current = undefined;\n }\n };\n\n runCycle();\n }, [clearAnnounceTimeout, messageQueue, setAnnounceTimeout, targetDocument]);\n\n const announce: AriaLiveAnnounceFn = React.useCallback(\n (message: string, options: AnnounceOptions = {}) => {\n const { alert = false, priority = 0, batchId } = options;\n\n // check if message is an alert\n if (alert) {\n // TODO: alert implementation\n // setAlertList([...alertList, message]);\n }\n\n const liveMessage: AriaLiveMessage = {\n message,\n createdAt: order.current++,\n priority,\n batchId,\n };\n\n // check if batchId exists\n if (batchId) {\n // update associated msg if it does\n const batchMessage = batchMessages.current.find(msg => msg.batchId === batchId);\n\n if (batchMessage) {\n // replace existing message in queue\n messageQueue.remove(batchMessage.message);\n\n // update list of existing batchIds w/ most recent message\n batchMessage.message = liveMessage;\n } else {\n // update list of existing batchIds, add new if doesn't already exist\n batchMessages.current = [...batchMessages.current, { batchId, message: liveMessage }];\n }\n }\n\n // add new message\n messageQueue.enqueue(liveMessage);\n queueMessage();\n },\n [messageQueue, queueMessage],\n );\n\n React.useEffect(() => {\n if (!targetDocument) {\n return;\n }\n\n const element = targetDocument.createElement('div');\n element.setAttribute('aria-live', 'assertive');\n\n Object.assign(element.style, VISUALLY_HIDDEN_STYLES);\n targetDocument.body.append(element);\n\n elementRef.current = element;\n\n return () => {\n element.remove();\n elementRef.current = null;\n };\n }, [targetDocument]);\n\n return announce;\n};\n"],"names":["useFluent_unstable","useFluent","createPriorityQueue","useTimeout","React","MESSAGE_DURATION","VISUALLY_HIDDEN_STYLES","clip","height","margin","width","position","overflow","textWrap","useDomAnnounce_unstable","targetDocument","timeoutRef","useRef","undefined","setAnnounceTimeout","clearAnnounceTimeout","elementRef","order","batchMessages","messageQueue","useState","a","b","priority","createdAt","queueMessage","useCallback","current","runCycle","peek","wrappingEl","createElement","innerText","all","filter","msg","message","trim","length","reduce","prevText","currMsg","appendChild","clear","textContent","announce","options","alert","batchId","liveMessage","batchMessage","find","remove","enqueue","useEffect","element","setAttribute","Object","assign","style","body","append"],"mappings":"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"}
@@ -7,6 +7,7 @@ const activeDescendantContextDefaultValue = {
7
7
  find: noop,
8
8
  first: noop,
9
9
  focus: noop,
10
+ focusLastActive: noop,
10
11
  last: noop,
11
12
  next: noop,
12
13
  prev: noop,
@@ -1 +1 @@
1
- {"version":3,"sources":["ActiveDescendantContext.ts"],"sourcesContent":["import * as React from 'react';\nimport { ActiveDescendantImperativeRef } from './types';\n\nexport type ActiveDescendantContextValue = {\n controller: ActiveDescendantImperativeRef;\n};\n\nconst noop = () => undefined;\n\nconst activeDescendantContextDefaultValue: ActiveDescendantContextValue = {\n controller: {\n active: noop,\n blur: noop,\n find: noop,\n first: noop,\n focus: noop,\n last: noop,\n next: noop,\n prev: noop,\n showAttributes: noop,\n hideAttributes: noop,\n },\n};\n\nconst ActiveDescendantContext = React.createContext<ActiveDescendantContextValue | undefined>(undefined);\n\nexport const ActiveDescendantContextProvider = ActiveDescendantContext.Provider;\nexport const useActiveDescendantContext = () =>\n React.useContext(ActiveDescendantContext) ?? activeDescendantContextDefaultValue;\nexport const useHasParentActiveDescendantContext = () => !!React.useContext(ActiveDescendantContext);\n"],"names":["React","noop","undefined","activeDescendantContextDefaultValue","controller","active","blur","find","first","focus","last","next","prev","showAttributes","hideAttributes","ActiveDescendantContext","createContext","ActiveDescendantContextProvider","Provider","useActiveDescendantContext","useContext","useHasParentActiveDescendantContext"],"mappings":"AAAA,YAAYA,WAAW,QAAQ;AAO/B,MAAMC,OAAO,IAAMC;AAEnB,MAAMC,sCAAoE;IACxEC,YAAY;QACVC,QAAQJ;QACRK,MAAML;QACNM,MAAMN;QACNO,OAAOP;QACPQ,OAAOR;QACPS,MAAMT;QACNU,MAAMV;QACNW,MAAMX;QACNY,gBAAgBZ;QAChBa,gBAAgBb;IAClB;AACF;AAEA,MAAMc,0BAA0Bf,MAAMgB,aAAa,CAA2Cd;AAE9F,OAAO,MAAMe,kCAAkCF,wBAAwBG,QAAQ,CAAC;AAChF,OAAO,MAAMC,6BAA6B;QACxCnB;WAAAA,CAAAA,oBAAAA,MAAMoB,UAAU,CAACL,sCAAjBf,+BAAAA,oBAA6CG;AAAkC,EAAE;AACnF,OAAO,MAAMkB,sCAAsC,IAAM,CAAC,CAACrB,MAAMoB,UAAU,CAACL,yBAAyB"}
1
+ {"version":3,"sources":["ActiveDescendantContext.ts"],"sourcesContent":["import * as React from 'react';\nimport { ActiveDescendantImperativeRef } from './types';\n\nexport type ActiveDescendantContextValue = {\n controller: ActiveDescendantImperativeRef;\n};\n\nconst noop = () => undefined;\n\nconst activeDescendantContextDefaultValue: ActiveDescendantContextValue = {\n controller: {\n active: noop,\n blur: noop,\n find: noop,\n first: noop,\n focus: noop,\n focusLastActive: noop,\n last: noop,\n next: noop,\n prev: noop,\n showAttributes: noop,\n hideAttributes: noop,\n },\n};\n\nconst ActiveDescendantContext = React.createContext<ActiveDescendantContextValue | undefined>(undefined);\n\nexport const ActiveDescendantContextProvider = ActiveDescendantContext.Provider;\nexport const useActiveDescendantContext = () =>\n React.useContext(ActiveDescendantContext) ?? activeDescendantContextDefaultValue;\nexport const useHasParentActiveDescendantContext = () => !!React.useContext(ActiveDescendantContext);\n"],"names":["React","noop","undefined","activeDescendantContextDefaultValue","controller","active","blur","find","first","focus","focusLastActive","last","next","prev","showAttributes","hideAttributes","ActiveDescendantContext","createContext","ActiveDescendantContextProvider","Provider","useActiveDescendantContext","useContext","useHasParentActiveDescendantContext"],"mappings":"AAAA,YAAYA,WAAW,QAAQ;AAO/B,MAAMC,OAAO,IAAMC;AAEnB,MAAMC,sCAAoE;IACxEC,YAAY;QACVC,QAAQJ;QACRK,MAAML;QACNM,MAAMN;QACNO,OAAOP;QACPQ,OAAOR;QACPS,iBAAiBT;QACjBU,MAAMV;QACNW,MAAMX;QACNY,MAAMZ;QACNa,gBAAgBb;QAChBc,gBAAgBd;IAClB;AACF;AAEA,MAAMe,0BAA0BhB,MAAMiB,aAAa,CAA2Cf;AAE9F,OAAO,MAAMgB,kCAAkCF,wBAAwBG,QAAQ,CAAC;AAChF,OAAO,MAAMC,6BAA6B;QACxCpB;WAAAA,CAAAA,oBAAAA,MAAMqB,UAAU,CAACL,sCAAjBhB,+BAAAA,oBAA6CG;AAAkC,EAAE;AACnF,OAAO,MAAMmB,sCAAsC,IAAM,CAAC,CAACtB,MAAMqB,UAAU,CAACL,yBAAyB"}
@@ -1,19 +1,54 @@
1
- export const scrollIntoView = (target, scrollParent)=>{
2
- if (!target || !scrollParent) {
1
+ export const scrollIntoView = (target)=>{
2
+ if (!target) {
3
3
  return;
4
4
  }
5
- if (scrollParent.offsetHeight >= scrollParent.scrollHeight) {
5
+ const scrollParent = findScrollableParent(target.parentElement);
6
+ if (!scrollParent) {
6
7
  return;
7
8
  }
8
- const { offsetHeight, offsetTop } = target;
9
+ const { offsetHeight } = target;
10
+ const offsetTop = getTotalOffsetTop(target, scrollParent);
11
+ const { scrollMarginTop, scrollMarginBottom } = getScrollMargins(target);
9
12
  const { offsetHeight: parentOffsetHeight, scrollTop } = scrollParent;
10
- const isAbove = offsetTop < scrollTop;
11
- const isBelow = offsetTop + offsetHeight > scrollTop + parentOffsetHeight;
13
+ const isAbove = offsetTop - scrollMarginTop < scrollTop;
14
+ const isBelow = offsetTop + offsetHeight + scrollMarginBottom > scrollTop + parentOffsetHeight;
12
15
  const buffer = 2;
13
16
  if (isAbove) {
14
- scrollParent.scrollTo(0, offsetTop - buffer);
17
+ scrollParent.scrollTo(0, offsetTop - scrollMarginTop - buffer);
18
+ } else if (isBelow) {
19
+ scrollParent.scrollTo(0, offsetTop + offsetHeight + scrollMarginBottom - parentOffsetHeight + buffer);
15
20
  }
16
- if (isBelow) {
17
- scrollParent.scrollTo(0, offsetTop - parentOffsetHeight + offsetHeight + buffer);
21
+ };
22
+ const findScrollableParent = (element)=>{
23
+ if (!element) {
24
+ return null;
25
+ }
26
+ if (element.scrollHeight > element.offsetHeight) {
27
+ return element;
28
+ }
29
+ return findScrollableParent(element.parentElement);
30
+ };
31
+ const getTotalOffsetTop = (element, scrollParent)=>{
32
+ if (!element || element === scrollParent) {
33
+ return 0;
18
34
  }
35
+ if (element.contains(scrollParent)) {
36
+ // subtract the scroll parent's offset top from the running total if the offsetParent is above it
37
+ return scrollParent.offsetTop * -1;
38
+ }
39
+ return element.offsetTop + getTotalOffsetTop(element.offsetParent, scrollParent);
40
+ };
41
+ const getScrollMargins = (element)=>{
42
+ const computedStyles = getComputedStyle(element);
43
+ var _getIntValueOfComputedStyle;
44
+ const scrollMarginTop = (_getIntValueOfComputedStyle = getIntValueOfComputedStyle(computedStyles.scrollMarginTop)) !== null && _getIntValueOfComputedStyle !== void 0 ? _getIntValueOfComputedStyle : getIntValueOfComputedStyle(computedStyles.scrollMarginBlockStart);
45
+ var _getIntValueOfComputedStyle1;
46
+ const scrollMarginBottom = (_getIntValueOfComputedStyle1 = getIntValueOfComputedStyle(computedStyles.scrollMarginBottom)) !== null && _getIntValueOfComputedStyle1 !== void 0 ? _getIntValueOfComputedStyle1 : getIntValueOfComputedStyle(computedStyles.scrollMarginBlockEnd);
47
+ return {
48
+ scrollMarginTop,
49
+ scrollMarginBottom
50
+ };
51
+ };
52
+ const getIntValueOfComputedStyle = (computedStyle)=>{
53
+ return computedStyle ? parseInt(computedStyle, 10) : 0;
19
54
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["scrollIntoView.ts"],"sourcesContent":["export const scrollIntoView = (\n target: HTMLElement | null | undefined,\n scrollParent: HTMLElement | null | undefined,\n) => {\n if (!target || !scrollParent) {\n return;\n }\n\n if (scrollParent.offsetHeight >= scrollParent.scrollHeight) {\n return;\n }\n\n const { offsetHeight, offsetTop } = target;\n const { offsetHeight: parentOffsetHeight, scrollTop } = scrollParent;\n\n const isAbove = offsetTop < scrollTop;\n const isBelow = offsetTop + offsetHeight > scrollTop + parentOffsetHeight;\n\n const buffer = 2;\n\n if (isAbove) {\n scrollParent.scrollTo(0, offsetTop - buffer);\n }\n\n if (isBelow) {\n scrollParent.scrollTo(0, offsetTop - parentOffsetHeight + offsetHeight + buffer);\n }\n};\n"],"names":["scrollIntoView","target","scrollParent","offsetHeight","scrollHeight","offsetTop","parentOffsetHeight","scrollTop","isAbove","isBelow","buffer","scrollTo"],"mappings":"AAAA,OAAO,MAAMA,iBAAiB,CAC5BC,QACAC;IAEA,IAAI,CAACD,UAAU,CAACC,cAAc;QAC5B;IACF;IAEA,IAAIA,aAAaC,YAAY,IAAID,aAAaE,YAAY,EAAE;QAC1D;IACF;IAEA,MAAM,EAAED,YAAY,EAAEE,SAAS,EAAE,GAAGJ;IACpC,MAAM,EAAEE,cAAcG,kBAAkB,EAAEC,SAAS,EAAE,GAAGL;IAExD,MAAMM,UAAUH,YAAYE;IAC5B,MAAME,UAAUJ,YAAYF,eAAeI,YAAYD;IAEvD,MAAMI,SAAS;IAEf,IAAIF,SAAS;QACXN,aAAaS,QAAQ,CAAC,GAAGN,YAAYK;IACvC;IAEA,IAAID,SAAS;QACXP,aAAaS,QAAQ,CAAC,GAAGN,YAAYC,qBAAqBH,eAAeO;IAC3E;AACF,EAAE"}
1
+ {"version":3,"sources":["scrollIntoView.ts"],"sourcesContent":["export const scrollIntoView = (target: HTMLElement | null | undefined) => {\n if (!target) {\n return;\n }\n\n const scrollParent = findScrollableParent(target.parentElement as HTMLElement);\n if (!scrollParent) {\n return;\n }\n\n const { offsetHeight } = target;\n const offsetTop = getTotalOffsetTop(target, scrollParent);\n\n const { scrollMarginTop, scrollMarginBottom } = getScrollMargins(target);\n\n const { offsetHeight: parentOffsetHeight, scrollTop } = scrollParent;\n\n const isAbove = offsetTop - scrollMarginTop < scrollTop;\n const isBelow = offsetTop + offsetHeight + scrollMarginBottom > scrollTop + parentOffsetHeight;\n\n const buffer = 2;\n\n if (isAbove) {\n scrollParent.scrollTo(0, offsetTop - scrollMarginTop - buffer);\n } else if (isBelow) {\n scrollParent.scrollTo(0, offsetTop + offsetHeight + scrollMarginBottom - parentOffsetHeight + buffer);\n }\n};\n\nconst findScrollableParent = (element: HTMLElement | null): HTMLElement | null => {\n if (!element) {\n return null;\n }\n\n if (element.scrollHeight > element.offsetHeight) {\n return element;\n }\n\n return findScrollableParent(element.parentElement);\n};\n\nconst getTotalOffsetTop = (element: HTMLElement, scrollParent: HTMLElement): number => {\n if (!element || element === scrollParent) {\n return 0;\n }\n\n if (element.contains(scrollParent)) {\n // subtract the scroll parent's offset top from the running total if the offsetParent is above it\n return scrollParent.offsetTop * -1;\n }\n\n return element.offsetTop + getTotalOffsetTop(element.offsetParent as HTMLElement, scrollParent);\n};\n\nconst getScrollMargins = (element: HTMLElement) => {\n const computedStyles = getComputedStyle(element);\n const scrollMarginTop =\n getIntValueOfComputedStyle(computedStyles.scrollMarginTop) ??\n getIntValueOfComputedStyle(computedStyles.scrollMarginBlockStart);\n const scrollMarginBottom =\n getIntValueOfComputedStyle(computedStyles.scrollMarginBottom) ??\n getIntValueOfComputedStyle(computedStyles.scrollMarginBlockEnd);\n return {\n scrollMarginTop,\n scrollMarginBottom,\n };\n};\n\nconst getIntValueOfComputedStyle = (computedStyle: string) => {\n return computedStyle ? parseInt(computedStyle, 10) : 0;\n};\n"],"names":["scrollIntoView","target","scrollParent","findScrollableParent","parentElement","offsetHeight","offsetTop","getTotalOffsetTop","scrollMarginTop","scrollMarginBottom","getScrollMargins","parentOffsetHeight","scrollTop","isAbove","isBelow","buffer","scrollTo","element","scrollHeight","contains","offsetParent","computedStyles","getComputedStyle","getIntValueOfComputedStyle","scrollMarginBlockStart","scrollMarginBlockEnd","computedStyle","parseInt"],"mappings":"AAAA,OAAO,MAAMA,iBAAiB,CAACC;IAC7B,IAAI,CAACA,QAAQ;QACX;IACF;IAEA,MAAMC,eAAeC,qBAAqBF,OAAOG,aAAa;IAC9D,IAAI,CAACF,cAAc;QACjB;IACF;IAEA,MAAM,EAAEG,YAAY,EAAE,GAAGJ;IACzB,MAAMK,YAAYC,kBAAkBN,QAAQC;IAE5C,MAAM,EAAEM,eAAe,EAAEC,kBAAkB,EAAE,GAAGC,iBAAiBT;IAEjE,MAAM,EAAEI,cAAcM,kBAAkB,EAAEC,SAAS,EAAE,GAAGV;IAExD,MAAMW,UAAUP,YAAYE,kBAAkBI;IAC9C,MAAME,UAAUR,YAAYD,eAAeI,qBAAqBG,YAAYD;IAE5E,MAAMI,SAAS;IAEf,IAAIF,SAAS;QACXX,aAAac,QAAQ,CAAC,GAAGV,YAAYE,kBAAkBO;IACzD,OAAO,IAAID,SAAS;QAClBZ,aAAac,QAAQ,CAAC,GAAGV,YAAYD,eAAeI,qBAAqBE,qBAAqBI;IAChG;AACF,EAAE;AAEF,MAAMZ,uBAAuB,CAACc;IAC5B,IAAI,CAACA,SAAS;QACZ,OAAO;IACT;IAEA,IAAIA,QAAQC,YAAY,GAAGD,QAAQZ,YAAY,EAAE;QAC/C,OAAOY;IACT;IAEA,OAAOd,qBAAqBc,QAAQb,aAAa;AACnD;AAEA,MAAMG,oBAAoB,CAACU,SAAsBf;IAC/C,IAAI,CAACe,WAAWA,YAAYf,cAAc;QACxC,OAAO;IACT;IAEA,IAAIe,QAAQE,QAAQ,CAACjB,eAAe;QAClC,iGAAiG;QACjG,OAAOA,aAAaI,SAAS,GAAG,CAAC;IACnC;IAEA,OAAOW,QAAQX,SAAS,GAAGC,kBAAkBU,QAAQG,YAAY,EAAiBlB;AACpF;AAEA,MAAMQ,mBAAmB,CAACO;IACxB,MAAMI,iBAAiBC,iBAAiBL;QAEtCM;IADF,MAAMf,kBACJe,CAAAA,8BAAAA,2BAA2BF,eAAeb,eAAe,eAAzDe,yCAAAA,8BACAA,2BAA2BF,eAAeG,sBAAsB;QAEhED;IADF,MAAMd,qBACJc,CAAAA,+BAAAA,2BAA2BF,eAAeZ,kBAAkB,eAA5Dc,0CAAAA,+BACAA,2BAA2BF,eAAeI,oBAAoB;IAChE,OAAO;QACLjB;QACAC;IACF;AACF;AAEA,MAAMc,6BAA6B,CAACG;IAClC,OAAOA,gBAAgBC,SAASD,eAAe,MAAM;AACvD"}
@@ -1 +1 @@
1
- {"version":3,"sources":["types.ts"],"sourcesContent":["import * as React from 'react';\n\nexport interface ActiveDescendantImperativeRef {\n first: (options?: IteratorOptions) => string | undefined;\n last: (options?: IteratorOptions) => string | undefined;\n next: (options?: IteratorOptions) => string | undefined;\n prev: (options?: IteratorOptions) => string | undefined;\n find: (predicate: (id: string) => boolean, options?: IteratorOptions & FindOptions) => string | undefined;\n blur: () => void;\n active: () => string | undefined;\n focus: (id: string) => void;\n hideAttributes: () => void;\n showAttributes: () => void;\n}\n\nexport interface ActiveDescendantOptions {\n /**\n * @param el - HTML element to test\n * @returns whether the element can be an active descendant\n */\n matchOption: (el: HTMLElement) => boolean;\n /**\n * Forward imperative refs when exposing functionality from a React component\n */\n imperativeRef?: React.RefObject<ActiveDescendantImperativeRef>;\n}\n\nexport interface FindOptions {\n /**\n * Starts the search from a specific id\n */\n startFrom?: string;\n}\n\nexport interface UseActiveDescendantReturn<\n TActiveParentElement extends HTMLElement = HTMLElement,\n TListboxElement extends HTMLElement = HTMLElement,\n> {\n /**\n * Attach this to the element that contains all active descendants\n */\n listboxRef: React.Ref<TListboxElement>;\n /**\n * Attach this to the element that has an active descendant\n */\n activeParentRef: React.Ref<TActiveParentElement>;\n /**\n * Imperative functions to manage active descendants within the listboxRef\n */\n controller: ActiveDescendantImperativeRef;\n}\n\nexport interface IteratorOptions {\n /**\n * When passive, the active descendant is changed\n * @default false\n */\n passive?: boolean;\n}\n"],"names":["React"],"mappings":"AAAA,YAAYA,WAAW,QAAQ"}
1
+ {"version":3,"sources":["types.ts"],"sourcesContent":["import * as React from 'react';\n\nexport interface ActiveDescendantImperativeRef {\n first: (options?: IteratorOptions) => string | undefined;\n last: (options?: IteratorOptions) => string | undefined;\n next: (options?: IteratorOptions) => string | undefined;\n prev: (options?: IteratorOptions) => string | undefined;\n find: (predicate: (id: string) => boolean, options?: IteratorOptions & FindOptions) => string | undefined;\n blur: () => void;\n active: () => string | undefined;\n focus: (id: string) => void;\n /**\n * Focuses the last active descendant, if it still exists\n * @returns true if the last active descendant was focused\n */\n focusLastActive: () => boolean | undefined;\n hideAttributes: () => void;\n showAttributes: () => void;\n}\n\nexport interface ActiveDescendantOptions {\n /**\n * @param el - HTML element to test\n * @returns whether the element can be an active descendant\n */\n matchOption: (el: HTMLElement) => boolean;\n /**\n * Forward imperative refs when exposing functionality from a React component\n */\n imperativeRef?: React.RefObject<ActiveDescendantImperativeRef>;\n}\n\nexport interface FindOptions {\n /**\n * Starts the search from a specific id\n */\n startFrom?: string;\n}\n\nexport interface UseActiveDescendantReturn<\n TActiveParentElement extends HTMLElement = HTMLElement,\n TListboxElement extends HTMLElement = HTMLElement,\n> {\n /**\n * Attach this to the element that contains all active descendants\n */\n listboxRef: React.Ref<TListboxElement>;\n /**\n * Attach this to the element that has an active descendant\n */\n activeParentRef: React.Ref<TActiveParentElement>;\n /**\n * Imperative functions to manage active descendants within the listboxRef\n */\n controller: ActiveDescendantImperativeRef;\n}\n\nexport interface IteratorOptions {\n /**\n * When passive, the active descendant is changed\n * @default false\n */\n passive?: boolean;\n}\n"],"names":["React"],"mappings":"AAAA,YAAYA,WAAW,QAAQ"}
@@ -4,10 +4,17 @@ import { useOnKeyboardNavigationChange } from '@fluentui/react-tabster';
4
4
  import { useOptionWalker } from './useOptionWalker';
5
5
  import { ACTIVEDESCENDANT_ATTRIBUTE, ACTIVEDESCENDANT_FOCUSVISIBLE_ATTRIBUTE } from './constants';
6
6
  import { scrollIntoView } from './scrollIntoView';
7
+ export const createActiveDescendantChangeEvent = (detail)=>new CustomEvent('activedescendantchange', {
8
+ bubbles: true,
9
+ cancelable: false,
10
+ composed: true,
11
+ detail
12
+ });
7
13
  export function useActiveDescendant(options) {
8
14
  const { imperativeRef, matchOption: matchOptionUnstable } = options;
9
15
  const focusVisibleRef = React.useRef(false);
10
16
  const activeIdRef = React.useRef(null);
17
+ const lastActiveIdRef = React.useRef(null);
11
18
  const activeParentRef = React.useRef(null);
12
19
  const attributeVisibilityRef = React.useRef(true);
13
20
  const removeAttribute = React.useCallback(()=>{
@@ -53,7 +60,10 @@ export function useActiveDescendant(options) {
53
60
  active.removeAttribute(ACTIVEDESCENDANT_FOCUSVISIBLE_ATTRIBUTE);
54
61
  }
55
62
  removeAttribute();
63
+ lastActiveIdRef.current = activeIdRef.current;
56
64
  activeIdRef.current = null;
65
+ var _active_id;
66
+ return (_active_id = active === null || active === void 0 ? void 0 : active.id) !== null && _active_id !== void 0 ? _active_id : null;
57
67
  }, [
58
68
  getActiveDescendant,
59
69
  removeAttribute
@@ -62,15 +72,19 @@ export function useActiveDescendant(options) {
62
72
  if (!nextActive) {
63
73
  return;
64
74
  }
65
- blurActiveDescendant();
66
- scrollIntoView(nextActive, listboxRef.current);
75
+ const previousActiveId = blurActiveDescendant();
76
+ scrollIntoView(nextActive);
67
77
  setAttribute(nextActive.id);
68
78
  nextActive.setAttribute(ACTIVEDESCENDANT_ATTRIBUTE, '');
69
79
  if (focusVisibleRef.current) {
70
80
  nextActive.setAttribute(ACTIVEDESCENDANT_FOCUSVISIBLE_ATTRIBUTE, '');
71
81
  }
82
+ const event = createActiveDescendantChangeEvent({
83
+ id: nextActive.id,
84
+ previousId: previousActiveId
85
+ });
86
+ nextActive.dispatchEvent(event);
72
87
  }, [
73
- listboxRef,
74
88
  blurActiveDescendant,
75
89
  setAttribute
76
90
  ]);
@@ -129,6 +143,16 @@ export function useActiveDescendant(options) {
129
143
  focusActiveDescendant(target);
130
144
  }
131
145
  },
146
+ focusLastActive: ()=>{
147
+ if (!listboxRef.current || !lastActiveIdRef.current) {
148
+ return;
149
+ }
150
+ const target = listboxRef.current.querySelector(`#${lastActiveIdRef.current}`);
151
+ if (target) {
152
+ focusActiveDescendant(target);
153
+ return true;
154
+ }
155
+ },
132
156
  find (predicate, { passive, startFrom } = {}) {
133
157
  const target = optionWalker.find(predicate, startFrom);
134
158
  if (!passive) {