Skip to content

Commit b5d9182

Browse files
Merge pull request #5963 from liferay/LPD-47056
feat(@clayui/core): LPD-47056 Add Keyboard Interactions to the Icon Selector
2 parents b563539 + 1f7770e commit b5d9182

File tree

5 files changed

+256
-115
lines changed

5 files changed

+256
-115
lines changed

packages/clay-core/docs/icon-selector.mdx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@ Icon Selector is a field type that allows users to choose an icon from a predefi
1616

1717
```jsx preview
1818
import {IconSelector} from '@clayui/core';
19+
import React from 'react';
1920

2021
import '@clayui/css/lib/css/atlas.css';
2122

23+
const spritemap = require('@clayui/css/lib/images/icons/icons.svg');
24+
2225
export default function App() {
2326
return (
2427
<div className="p-4">
25-
<IconSelector />
28+
<IconSelector spritemap={spritemap} />
2629
</div>
2730
);
2831
}

packages/clay-core/src/drop-down/Menu.tsx

Lines changed: 8 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,17 @@ import {
88
InternalDispatch,
99
Keys,
1010
Overlay,
11-
getFocusableList,
1211
useControlledState,
1312
useId,
13+
useInteractionFocus,
1414
useNavigation,
1515
useOverlayPosition,
1616
} from '@clayui/shared';
1717
import classNames from 'classnames';
18-
import React, {
19-
useCallback,
20-
useEffect,
21-
useImperativeHandle,
22-
useRef,
23-
} from 'react';
18+
import React, {useCallback, useImperativeHandle, useRef} from 'react';
2419

2520
import {Collection, useCollection, useVirtual} from '../collection';
21+
import {FocusMenu} from '../focus-trap';
2622

2723
import type {ICollectionProps} from '../collection';
2824

@@ -126,6 +122,9 @@ function MenuInner<T extends Record<string, unknown> | string | number>(
126122
const menuRef = useRef<HTMLDivElement>(null);
127123
const triggerRef = useRef<HTMLButtonElement | null>(null);
128124

125+
// Pre-initializes events for the Focus Menu.
126+
useInteractionFocus();
127+
129128
useImperativeHandle(ref, () => menuRef.current, []);
130129

131130
const [active, setActive] = useControlledState({
@@ -287,23 +286,8 @@ function MenuInner<T extends Record<string, unknown> | string | number>(
287286
style={style}
288287
>
289288
<FocusMenu
290-
onRender={() => {
291-
// After a few milliseconds querying the elements in the DOM
292-
// inside the menu. This especially when the menu is not
293-
// rendered yet only after the menu is opened, React needs
294-
// to commit the changes to the DOM so that the elements are
295-
// visible and we can move the focus.
296-
setTimeout(() => {
297-
const list = getFocusableList(
298-
menuRef,
299-
UNSAFE_focusableElements
300-
);
301-
302-
if (list.length) {
303-
list[0]!.focus();
304-
}
305-
}, 10);
306-
}}
289+
focusableElements={UNSAFE_focusableElements}
290+
menuRef={menuRef}
307291
>
308292
<Collection<T>
309293
{...otherProps}
@@ -357,19 +341,6 @@ function MenuInner<T extends Record<string, unknown> | string | number>(
357341
);
358342
}
359343

360-
type FocusMenuProps<T> = {
361-
children: T;
362-
onRender: () => void;
363-
};
364-
365-
export function FocusMenu<T>({children, onRender}: FocusMenuProps<T>) {
366-
useEffect(() => {
367-
onRender();
368-
}, []);
369-
370-
return children;
371-
}
372-
373344
type ForwardRef = {
374345
displayName: string;
375346
<T>(props: Props<T> & {ref?: React.Ref<HTMLDivElement>}): JSX.Element;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* SPDX-FileCopyrightText: © 2025 Liferay, Inc. <https://liferay.com>
3+
* SPDX-License-Identifier: BSD-3-Clause
4+
*/
5+
6+
import {
7+
FOCUSABLE_ELEMENTS,
8+
getFocusableList,
9+
useInteractionFocus,
10+
} from '@clayui/shared';
11+
import React, {useEffect} from 'react';
12+
13+
type FocusMenuProps<T, E extends HTMLElement> = {
14+
children: T;
15+
menuRef: React.MutableRefObject<E> | React.RefObject<E>;
16+
focusableElements?: Array<string>;
17+
};
18+
19+
/**
20+
* TODO: Move the implementation to be part of the useNavigation hook so that
21+
* it is an internal function that can be called to focus on the first item.
22+
*/
23+
export function FocusMenu<T, E extends HTMLElement>({
24+
children,
25+
menuRef,
26+
focusableElements = FOCUSABLE_ELEMENTS,
27+
}: FocusMenuProps<T, E>): JSX.Element {
28+
const {isFocusVisible} = useInteractionFocus();
29+
30+
useEffect(() => {
31+
if (!isFocusVisible()) {
32+
return;
33+
}
34+
35+
// After a few milliseconds querying the elements in the DOM
36+
// inside the menu. This especially when the menu is not
37+
// rendered yet only after the menu is opened, React needs
38+
// to commit the changes to the DOM so that the elements are
39+
// visible and we can move the focus.
40+
setTimeout(() => {
41+
const list = getFocusableList(menuRef, focusableElements);
42+
43+
if (list.length) {
44+
list[0]!.focus();
45+
}
46+
}, 10);
47+
}, []);
48+
49+
return <>{children}</>;
50+
}

packages/clay-core/src/focus-trap/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
*/
55

66
export {FocusTrap} from './FocusTrap';
7+
export {FocusMenu} from './FocusMenu';

0 commit comments

Comments
 (0)