@openmrs/esm-styleguide 8.0.1-pre.3758 → 8.0.1-pre.3762
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/.turbo/turbo-build.log +1 -1
- package/dist/custom-overflow-menu/custom-overflow-menu.component.d.ts +6 -0
- package/dist/custom-overflow-menu/custom-overflow-menu.component.d.ts.map +1 -1
- package/dist/patient-banner/actions-menu/patient-banner-actions-menu.component.d.ts +7 -1
- package/dist/patient-banner/actions-menu/patient-banner-actions-menu.component.d.ts.map +1 -1
- package/package.json +12 -12
- package/src/custom-overflow-menu/custom-overflow-menu.component.tsx +96 -10
- package/src/custom-overflow-menu/custom-overflow-menu.test.tsx +56 -3
- package/src/patient-banner/actions-menu/patient-banner-actions-menu.component.tsx +114 -55
package/.turbo/turbo-build.log
CHANGED
|
@@ -13,6 +13,6 @@
|
|
|
13
13
|
[0] │ You can limit the size of your bundles by using import() to lazy load some parts of your application.
|
|
14
14
|
[0] │ For more info visit https://www.rspack.dev/guide/optimization/code-splitting
|
|
15
15
|
[0]
|
|
16
|
-
[0] Rspack compiled with 3 warnings in 9.
|
|
16
|
+
[0] Rspack compiled with 3 warnings in 9.39 s
|
|
17
17
|
[0] rspack --mode=production exited with code 0
|
|
18
18
|
[1] tsc --project tsconfig.build.json exited with code 0
|
|
@@ -9,6 +9,12 @@ interface CustomOverflowMenuProps {
|
|
|
9
9
|
menuTitle: React.ReactNode;
|
|
10
10
|
children: React.ReactNode;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* A custom overflow menu that supports a text/icon trigger button instead of
|
|
14
|
+
* Carbon's icon-only OverflowMenu trigger. Uses CSS-based show/hide rather
|
|
15
|
+
* than Carbon's FloatingMenu portal, so keyboard behavior (Escape, arrow keys,
|
|
16
|
+
* auto-focus) is implemented here to match the WAI-ARIA menu button pattern.
|
|
17
|
+
*/
|
|
12
18
|
export declare function CustomOverflowMenu({ menuTitle, children }: CustomOverflowMenuProps): React.JSX.Element;
|
|
13
19
|
type OverflowMenuItemProps = ComponentProps<typeof OverflowMenuItem>;
|
|
14
20
|
export declare function CustomOverflowMenuItem(props: Omit<OverflowMenuItemProps, 'closeMenu'>): React.JSX.Element;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"custom-overflow-menu.component.d.ts","sourceRoot":"","sources":["../../src/custom-overflow-menu/custom-overflow-menu.component.tsx"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"custom-overflow-menu.component.d.ts","sourceRoot":"","sources":["../../src/custom-overflow-menu/custom-overflow-menu.component.tsx"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,OAAO,KAAK,EAAE,EAWZ,KAAK,cAAc,EACpB,MAAM,OAAO,CAAC;AAEf,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAIjD,UAAU,8BAA8B;IACtC,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB;AAID,wBAAgB,qBAAqB,mCAMpC;AAED,UAAU,uBAAuB;IAC/B,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,uBAAuB,qBAiHlF;AAED,KAAK,qBAAqB,GAAG,cAAc,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAErE,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,IAAI,CAAC,qBAAqB,EAAE,WAAW,CAAC,qBAGrF"}
|
|
@@ -10,5 +10,11 @@ export interface PatientBannerActionsMenuProps {
|
|
|
10
10
|
*/
|
|
11
11
|
additionalActionsSlotState?: object;
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Overflow menu for the patient banner whose items come from an ExtensionSlot
|
|
15
|
+
* rather than direct React children. Because cloneElement cannot inject props
|
|
16
|
+
* into extension-rendered components, arrow key navigation is handled at the
|
|
17
|
+
* container level via onKeyDown instead of delegating to Carbon's OverflowMenuItem.
|
|
18
|
+
*/
|
|
19
|
+
export declare function PatientBannerActionsMenu({ patient, patientUuid, actionsSlotName, additionalActionsSlotState, }: PatientBannerActionsMenuProps): React.JSX.Element | null;
|
|
14
20
|
//# sourceMappingURL=patient-banner-actions-menu.component.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"patient-banner-actions-menu.component.d.ts","sourceRoot":"","sources":["../../../src/patient-banner/actions-menu/patient-banner-actions-menu.component.tsx"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,OAAO,
|
|
1
|
+
{"version":3,"file":"patient-banner-actions-menu.component.d.ts","sourceRoot":"","sources":["../../../src/patient-banner/actions-menu/patient-banner-actions-menu.component.tsx"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,OAAO,KAAmE,MAAM,OAAO,CAAC;AAQxF,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,OAAO,EACP,WAAW,EACX,eAAe,EACf,0BAA0B,GAC3B,EAAE,6BAA6B,4BA0H/B"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmrs/esm-styleguide",
|
|
3
|
-
"version": "8.0.1-pre.
|
|
3
|
+
"version": "8.0.1-pre.3762",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "The styleguide for OpenMRS SPA",
|
|
6
6
|
"main": "dist/openmrs-esm-styleguide.js",
|
|
@@ -98,17 +98,17 @@
|
|
|
98
98
|
"swr": "2.x"
|
|
99
99
|
},
|
|
100
100
|
"devDependencies": {
|
|
101
|
-
"@openmrs/esm-api": "8.0.1-pre.
|
|
102
|
-
"@openmrs/esm-config": "8.0.1-pre.
|
|
103
|
-
"@openmrs/esm-emr-api": "8.0.1-pre.
|
|
104
|
-
"@openmrs/esm-error-handling": "8.0.1-pre.
|
|
105
|
-
"@openmrs/esm-extensions": "8.0.1-pre.
|
|
106
|
-
"@openmrs/esm-globals": "8.0.1-pre.
|
|
107
|
-
"@openmrs/esm-navigation": "8.0.1-pre.
|
|
108
|
-
"@openmrs/esm-react-utils": "8.0.1-pre.
|
|
109
|
-
"@openmrs/esm-state": "8.0.1-pre.
|
|
110
|
-
"@openmrs/esm-translations": "8.0.1-pre.
|
|
111
|
-
"@openmrs/esm-utils": "8.0.1-pre.
|
|
101
|
+
"@openmrs/esm-api": "8.0.1-pre.3762",
|
|
102
|
+
"@openmrs/esm-config": "8.0.1-pre.3762",
|
|
103
|
+
"@openmrs/esm-emr-api": "8.0.1-pre.3762",
|
|
104
|
+
"@openmrs/esm-error-handling": "8.0.1-pre.3762",
|
|
105
|
+
"@openmrs/esm-extensions": "8.0.1-pre.3762",
|
|
106
|
+
"@openmrs/esm-globals": "8.0.1-pre.3762",
|
|
107
|
+
"@openmrs/esm-navigation": "8.0.1-pre.3762",
|
|
108
|
+
"@openmrs/esm-react-utils": "8.0.1-pre.3762",
|
|
109
|
+
"@openmrs/esm-state": "8.0.1-pre.3762",
|
|
110
|
+
"@openmrs/esm-translations": "8.0.1-pre.3762",
|
|
111
|
+
"@openmrs/esm-utils": "8.0.1-pre.3762",
|
|
112
112
|
"@rspack/cli": "^1.3.11",
|
|
113
113
|
"@rspack/core": "^1.3.11",
|
|
114
114
|
"@types/geopattern": "^1.2.9",
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
/** @module @category UI */
|
|
2
|
-
import React, {
|
|
2
|
+
import React, {
|
|
3
|
+
Children,
|
|
4
|
+
cloneElement,
|
|
5
|
+
createContext,
|
|
6
|
+
isValidElement,
|
|
7
|
+
useCallback,
|
|
8
|
+
useContext,
|
|
9
|
+
useEffect,
|
|
10
|
+
useId,
|
|
11
|
+
useMemo,
|
|
12
|
+
useRef,
|
|
13
|
+
type ComponentProps,
|
|
14
|
+
} from 'react';
|
|
3
15
|
import classNames from 'classnames';
|
|
4
16
|
import { OverflowMenuItem } from '@carbon/react';
|
|
5
17
|
import { useLayoutType, useOnClickOutside } from '@openmrs/esm-react-utils';
|
|
@@ -24,13 +36,81 @@ interface CustomOverflowMenuProps {
|
|
|
24
36
|
children: React.ReactNode;
|
|
25
37
|
}
|
|
26
38
|
|
|
39
|
+
/**
|
|
40
|
+
* A custom overflow menu that supports a text/icon trigger button instead of
|
|
41
|
+
* Carbon's icon-only OverflowMenu trigger. Uses CSS-based show/hide rather
|
|
42
|
+
* than Carbon's FloatingMenu portal, so keyboard behavior (Escape, arrow keys,
|
|
43
|
+
* auto-focus) is implemented here to match the WAI-ARIA menu button pattern.
|
|
44
|
+
*/
|
|
27
45
|
export function CustomOverflowMenu({ menuTitle, children }: CustomOverflowMenuProps) {
|
|
28
46
|
const [menuIsOpen, setMenuIsOpen] = React.useState(false);
|
|
29
47
|
const ref = useOnClickOutside<HTMLDivElement>(() => setMenuIsOpen(false), menuIsOpen);
|
|
30
48
|
const isTablet = useLayoutType() === 'tablet';
|
|
31
49
|
const toggleShowMenu = useCallback(() => setMenuIsOpen((state) => !state), []);
|
|
32
|
-
const
|
|
33
|
-
const
|
|
50
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
51
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
52
|
+
const uniqueId = useId();
|
|
53
|
+
const triggerId = `custom-overflow-menu-trigger-${uniqueId}`;
|
|
54
|
+
const menuId = `custom-overflow-menu-${uniqueId}`;
|
|
55
|
+
|
|
56
|
+
const closeMenuAndFocusTrigger = useCallback(() => {
|
|
57
|
+
setMenuIsOpen(false);
|
|
58
|
+
triggerRef.current?.focus();
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
const contextValue = useMemo(() => ({ closeMenu: closeMenuAndFocusTrigger }), [closeMenuAndFocusTrigger]);
|
|
62
|
+
|
|
63
|
+
const handleEscapeKey = useCallback(
|
|
64
|
+
(e: React.KeyboardEvent) => {
|
|
65
|
+
if (e.key === 'Escape' && menuIsOpen) {
|
|
66
|
+
e.stopPropagation();
|
|
67
|
+
closeMenuAndFocusTrigger();
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
[closeMenuAndFocusTrigger, menuIsOpen],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const childArray = Children.toArray(children);
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (menuIsOpen && menuRef.current) {
|
|
77
|
+
const firstItem = menuRef.current.querySelector<HTMLElement>('[role="menuitem"]:not([disabled])');
|
|
78
|
+
firstItem?.focus();
|
|
79
|
+
}
|
|
80
|
+
}, [menuIsOpen]);
|
|
81
|
+
|
|
82
|
+
const handleOverflowMenuItemFocus = useCallback(
|
|
83
|
+
({ currentIndex, direction }: { currentIndex?: number; direction: number }) => {
|
|
84
|
+
const enabledItems = menuRef.current?.querySelectorAll<HTMLElement>('[role="menuitem"]:not([disabled])');
|
|
85
|
+
if (!enabledItems?.length) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const activeItem =
|
|
90
|
+
(document.activeElement?.closest?.('[role="menuitem"]') as HTMLElement) ?? document.activeElement;
|
|
91
|
+
const currentPos = Array.from(enabledItems).indexOf(activeItem as HTMLElement);
|
|
92
|
+
if (currentPos === -1) {
|
|
93
|
+
enabledItems[direction > 0 ? 0 : enabledItems.length - 1]?.focus();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const nextPos = currentPos + direction;
|
|
98
|
+
const wrappedPos = nextPos < 0 ? enabledItems.length - 1 : nextPos >= enabledItems.length ? 0 : nextPos;
|
|
99
|
+
enabledItems[wrappedPos]?.focus();
|
|
100
|
+
},
|
|
101
|
+
[],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const enrichedChildren = childArray.map((child, index) => {
|
|
105
|
+
if (isValidElement(child)) {
|
|
106
|
+
return cloneElement(child as React.ReactElement<any>, {
|
|
107
|
+
closeMenu: closeMenuAndFocusTrigger,
|
|
108
|
+
handleOverflowMenuItemFocus,
|
|
109
|
+
index,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return child;
|
|
113
|
+
});
|
|
34
114
|
|
|
35
115
|
return (
|
|
36
116
|
<div data-overflow-menu className={classNames('cds--overflow-menu', styles.container)} ref={ref}>
|
|
@@ -42,11 +122,13 @@ export function CustomOverflowMenu({ menuTitle, children }: CustomOverflowMenuPr
|
|
|
42
122
|
{ 'cds--overflow-menu--open': menuIsOpen },
|
|
43
123
|
styles.overflowMenuButton,
|
|
44
124
|
)}
|
|
45
|
-
aria-
|
|
125
|
+
aria-controls={menuId}
|
|
46
126
|
aria-expanded={menuIsOpen}
|
|
47
|
-
|
|
48
|
-
|
|
127
|
+
aria-haspopup="true"
|
|
128
|
+
id={triggerId}
|
|
49
129
|
onClick={toggleShowMenu}
|
|
130
|
+
onKeyDown={handleEscapeKey}
|
|
131
|
+
ref={triggerRef}
|
|
50
132
|
>
|
|
51
133
|
{menuTitle}
|
|
52
134
|
</button>
|
|
@@ -54,16 +136,20 @@ export function CustomOverflowMenu({ menuTitle, children }: CustomOverflowMenuPr
|
|
|
54
136
|
className={classNames('cds--overflow-menu-options', 'cds--overflow-menu--flip', styles.menu, {
|
|
55
137
|
[styles.show]: menuIsOpen,
|
|
56
138
|
})}
|
|
57
|
-
|
|
139
|
+
aria-labelledby={triggerId}
|
|
58
140
|
data-floating-menu-direction="bottom"
|
|
141
|
+
id={menuId}
|
|
142
|
+
onKeyDown={handleEscapeKey}
|
|
143
|
+
ref={menuRef}
|
|
59
144
|
role="menu"
|
|
60
|
-
|
|
61
|
-
id="custom-actions-overflow-menu"
|
|
145
|
+
tabIndex={-1}
|
|
62
146
|
>
|
|
63
147
|
<ul
|
|
64
148
|
className={classNames('cds--overflow-menu-options__content', { 'cds--overflow-menu-options--lg': isTablet })}
|
|
65
149
|
>
|
|
66
|
-
<CustomOverflowMenuContext.Provider value={contextValue}>
|
|
150
|
+
<CustomOverflowMenuContext.Provider value={contextValue}>
|
|
151
|
+
{enrichedChildren}
|
|
152
|
+
</CustomOverflowMenuContext.Provider>
|
|
67
153
|
</ul>
|
|
68
154
|
<span />
|
|
69
155
|
</div>
|
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
3
|
import '@testing-library/jest-dom/vitest';
|
|
4
4
|
import userEvent from '@testing-library/user-event';
|
|
5
|
-
import { render, screen } from '@testing-library/react';
|
|
5
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
6
6
|
import { useLayoutType } from '@openmrs/esm-react-utils';
|
|
7
7
|
import { CustomOverflowMenu, CustomOverflowMenuItem, useCustomOverflowMenu } from './custom-overflow-menu.component';
|
|
8
8
|
|
|
@@ -79,8 +79,61 @@ describe('CustomOverflowMenu', () => {
|
|
|
79
79
|
|
|
80
80
|
expect(trigger).toHaveAttribute('aria-haspopup', 'true');
|
|
81
81
|
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
|
|
83
|
+
const menuId = trigger.getAttribute('aria-controls');
|
|
84
|
+
expect(menuId).toBeTruthy();
|
|
85
|
+
expect(menu).toHaveAttribute('id', menuId);
|
|
86
|
+
expect(menu).toHaveAttribute('aria-labelledby', trigger.id);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should generate unique IDs for multiple instances', () => {
|
|
90
|
+
render(
|
|
91
|
+
<>
|
|
92
|
+
<CustomOverflowMenu menuTitle="Menu A">
|
|
93
|
+
<li>Option 1</li>
|
|
94
|
+
</CustomOverflowMenu>
|
|
95
|
+
<CustomOverflowMenu menuTitle="Menu B">
|
|
96
|
+
<li>Option 2</li>
|
|
97
|
+
</CustomOverflowMenu>
|
|
98
|
+
</>,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const buttons = screen.getAllByRole('button');
|
|
102
|
+
expect(buttons[0].id).not.toBe(buttons[1].id);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should close menu and return focus to trigger on Escape key', async () => {
|
|
106
|
+
const user = userEvent.setup();
|
|
107
|
+
|
|
108
|
+
render(
|
|
109
|
+
<CustomOverflowMenu menuTitle="Menu">
|
|
110
|
+
<CustomOverflowMenuItem itemText="Option 1" />
|
|
111
|
+
</CustomOverflowMenu>,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const trigger = screen.getByRole('button', { name: /menu/i });
|
|
115
|
+
await user.click(trigger);
|
|
116
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
117
|
+
|
|
118
|
+
await user.keyboard('{Escape}');
|
|
119
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
120
|
+
expect(trigger).toHaveFocus();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should focus the first enabled menu item when opened', async () => {
|
|
124
|
+
const user = userEvent.setup();
|
|
125
|
+
|
|
126
|
+
render(
|
|
127
|
+
<CustomOverflowMenu menuTitle="Menu">
|
|
128
|
+
<CustomOverflowMenuItem itemText="Disabled Item" disabled />
|
|
129
|
+
<CustomOverflowMenuItem itemText="Enabled Item" />
|
|
130
|
+
</CustomOverflowMenu>,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
await user.click(screen.getByRole('button', { name: /menu/i }));
|
|
134
|
+
|
|
135
|
+
const menuItems = screen.getAllByRole('menuitem');
|
|
136
|
+
await waitFor(() => expect(menuItems[1]).toHaveFocus());
|
|
84
137
|
});
|
|
85
138
|
|
|
86
139
|
it('should have correct menu positioning attributes', () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @module @category UI */
|
|
2
|
-
import React, { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import React, { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import classNames from 'classnames';
|
|
4
4
|
import { OverflowMenuVertical } from '@carbon/react/icons';
|
|
5
5
|
import { ExtensionSlot, useExtensionSlot, useLayoutType, useOnClickOutside } from '@openmrs/esm-react-utils';
|
|
@@ -18,6 +18,12 @@ export interface PatientBannerActionsMenuProps {
|
|
|
18
18
|
additionalActionsSlotState?: object;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Overflow menu for the patient banner whose items come from an ExtensionSlot
|
|
23
|
+
* rather than direct React children. Because cloneElement cannot inject props
|
|
24
|
+
* into extension-rendered components, arrow key navigation is handled at the
|
|
25
|
+
* container level via onKeyDown instead of delegating to Carbon's OverflowMenuItem.
|
|
26
|
+
*/
|
|
21
27
|
export function PatientBannerActionsMenu({
|
|
22
28
|
patient,
|
|
23
29
|
patientUuid,
|
|
@@ -28,68 +34,121 @@ export function PatientBannerActionsMenu({
|
|
|
28
34
|
const { extensions: patientActions } = useExtensionSlot(actionsSlotName);
|
|
29
35
|
const isTablet = useLayoutType() === 'tablet';
|
|
30
36
|
const ref = useOnClickOutside<HTMLDivElement>(() => setMenuIsOpen(false), menuIsOpen);
|
|
37
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
38
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
39
|
+
const uniqueId = useId();
|
|
40
|
+
const triggerId = `patient-actions-menu-trigger-${uniqueId}`;
|
|
41
|
+
const menuId = `patient-actions-menu-${uniqueId}`;
|
|
31
42
|
|
|
32
43
|
const toggleShowMenu = useCallback(() => setMenuIsOpen((state) => !state), []);
|
|
33
|
-
|
|
44
|
+
|
|
45
|
+
const closeMenuAndFocusTrigger = useCallback(() => {
|
|
46
|
+
setMenuIsOpen(false);
|
|
47
|
+
triggerRef.current?.focus();
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const handleMenuKeyDown = useCallback(
|
|
51
|
+
(e: React.KeyboardEvent) => {
|
|
52
|
+
if (e.key === 'Escape' && menuIsOpen) {
|
|
53
|
+
e.stopPropagation();
|
|
54
|
+
closeMenuAndFocusTrigger();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if ((e.key === 'ArrowDown' || e.key === 'ArrowUp') && menuIsOpen) {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
const enabledItems = menuRef.current?.querySelectorAll<HTMLElement>('[role="menuitem"]:not([disabled])');
|
|
61
|
+
if (!enabledItems?.length) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const activeItem =
|
|
66
|
+
(document.activeElement?.closest?.('[role="menuitem"]') as HTMLElement) ?? document.activeElement;
|
|
67
|
+
const currentPos = Array.from(enabledItems).indexOf(activeItem as HTMLElement);
|
|
68
|
+
|
|
69
|
+
if (currentPos === -1) {
|
|
70
|
+
enabledItems[e.key === 'ArrowDown' ? 0 : enabledItems.length - 1]?.focus();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const direction = e.key === 'ArrowDown' ? 1 : -1;
|
|
75
|
+
const nextPos = currentPos + direction;
|
|
76
|
+
const wrappedPos = nextPos < 0 ? enabledItems.length - 1 : nextPos >= enabledItems.length ? 0 : nextPos;
|
|
77
|
+
enabledItems[wrappedPos]?.focus();
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
[closeMenuAndFocusTrigger, menuIsOpen],
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (menuIsOpen && menuRef.current) {
|
|
85
|
+
const firstItem = menuRef.current.querySelector<HTMLElement>('[role="menuitem"]:not([disabled])');
|
|
86
|
+
firstItem?.focus();
|
|
87
|
+
}
|
|
88
|
+
}, [menuIsOpen]);
|
|
34
89
|
|
|
35
90
|
const patientActionsSlotState = useMemo(
|
|
36
|
-
() => ({ patientUuid, patient, closeMenu, ...additionalActionsSlotState }),
|
|
37
|
-
[patientUuid, patient,
|
|
91
|
+
() => ({ patientUuid, patient, closeMenu: closeMenuAndFocusTrigger, ...additionalActionsSlotState }),
|
|
92
|
+
[patientUuid, patient, closeMenuAndFocusTrigger, additionalActionsSlotState],
|
|
38
93
|
);
|
|
39
94
|
|
|
95
|
+
if (patientActions.length === 0) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
40
99
|
return (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
100
|
+
<div className={styles.overflowMenuContainer}>
|
|
101
|
+
<div
|
|
102
|
+
data-overflow-menu
|
|
103
|
+
className={classNames('cds--overflow-menu', customOverflowMenuStyles.container)}
|
|
104
|
+
ref={ref}
|
|
105
|
+
>
|
|
106
|
+
<button
|
|
107
|
+
className={classNames(
|
|
108
|
+
'cds--btn',
|
|
109
|
+
'cds--btn--ghost',
|
|
110
|
+
'cds--overflow-menu__trigger',
|
|
111
|
+
{ 'cds--overflow-menu--open': menuIsOpen },
|
|
112
|
+
customOverflowMenuStyles.overflowMenuButton,
|
|
113
|
+
)}
|
|
114
|
+
aria-controls={menuId}
|
|
115
|
+
aria-expanded={menuIsOpen}
|
|
116
|
+
aria-haspopup="true"
|
|
117
|
+
id={triggerId}
|
|
118
|
+
onClick={toggleShowMenu}
|
|
119
|
+
onKeyDown={handleMenuKeyDown}
|
|
120
|
+
ref={triggerRef}
|
|
121
|
+
>
|
|
122
|
+
<span className={styles.actionsButtonText}>{getCoreTranslation('actions', 'Actions')}</span>{' '}
|
|
123
|
+
<OverflowMenuVertical size={16} style={{ marginLeft: '0.5rem', fill: '#78A9FF' }} />
|
|
124
|
+
</button>
|
|
125
|
+
<div
|
|
126
|
+
className={classNames(
|
|
127
|
+
'cds--overflow-menu-options',
|
|
128
|
+
'cds--overflow-menu--flip',
|
|
129
|
+
customOverflowMenuStyles.menu,
|
|
130
|
+
{
|
|
131
|
+
[customOverflowMenuStyles.show]: menuIsOpen,
|
|
132
|
+
},
|
|
133
|
+
)}
|
|
134
|
+
aria-labelledby={triggerId}
|
|
135
|
+
data-floating-menu-direction="bottom"
|
|
136
|
+
id={menuId}
|
|
137
|
+
onKeyDown={handleMenuKeyDown}
|
|
138
|
+
ref={menuRef}
|
|
139
|
+
role="menu"
|
|
140
|
+
tabIndex={-1}
|
|
141
|
+
>
|
|
142
|
+
<ul
|
|
143
|
+
className={classNames('cds--overflow-menu-options__content', {
|
|
144
|
+
'cds--overflow-menu-options--lg': isTablet,
|
|
145
|
+
})}
|
|
48
146
|
>
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
'cds--btn--ghost',
|
|
53
|
-
'cds--overflow-menu__trigger',
|
|
54
|
-
{ 'cds--overflow-menu--open': menuIsOpen },
|
|
55
|
-
customOverflowMenuStyles.overflowMenuButton,
|
|
56
|
-
)}
|
|
57
|
-
aria-haspopup="true"
|
|
58
|
-
aria-expanded={menuIsOpen}
|
|
59
|
-
id="custom-actions-overflow-menu-trigger"
|
|
60
|
-
aria-controls="custom-actions-overflow-menu"
|
|
61
|
-
onClick={toggleShowMenu}
|
|
62
|
-
>
|
|
63
|
-
<span className={styles.actionsButtonText}>{getCoreTranslation('actions', 'Actions')}</span>{' '}
|
|
64
|
-
<OverflowMenuVertical size={16} style={{ marginLeft: '0.5rem', fill: '#78A9FF' }} />
|
|
65
|
-
</button>
|
|
66
|
-
<div
|
|
67
|
-
className={classNames(
|
|
68
|
-
'cds--overflow-menu-options',
|
|
69
|
-
'cds--overflow-menu--flip',
|
|
70
|
-
customOverflowMenuStyles.menu,
|
|
71
|
-
{
|
|
72
|
-
[customOverflowMenuStyles.show]: menuIsOpen,
|
|
73
|
-
},
|
|
74
|
-
)}
|
|
75
|
-
tabIndex={0}
|
|
76
|
-
data-floating-menu-direction="bottom"
|
|
77
|
-
role="menu"
|
|
78
|
-
aria-labelledby="custom-actions-overflow-menu-trigger"
|
|
79
|
-
id="custom-actions-overflow-menu"
|
|
80
|
-
>
|
|
81
|
-
<ul
|
|
82
|
-
className={classNames('cds--overflow-menu-options__content', {
|
|
83
|
-
'cds--overflow-menu-options--lg': isTablet,
|
|
84
|
-
})}
|
|
85
|
-
>
|
|
86
|
-
<ExtensionSlot name={actionsSlotName} key={actionsSlotName} state={patientActionsSlotState} />
|
|
87
|
-
</ul>
|
|
88
|
-
<span />
|
|
89
|
-
</div>
|
|
90
|
-
</div>
|
|
147
|
+
<ExtensionSlot name={actionsSlotName} key={actionsSlotName} state={patientActionsSlotState} />
|
|
148
|
+
</ul>
|
|
149
|
+
<span />
|
|
91
150
|
</div>
|
|
92
|
-
|
|
93
|
-
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
94
153
|
);
|
|
95
154
|
}
|