Skip to content

Commit

Permalink
Merge pull request #5152 from matuzalemsteles/issue-5147
Browse files Browse the repository at this point in the history
fix(@clayui/drop-down): fix focus control error losing order and wrong behavior of Esc
  • Loading branch information
matuzalemsteles authored Oct 21, 2022
2 parents 121119b + 9ecc8e4 commit 89c87da
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 115 deletions.
191 changes: 90 additions & 101 deletions packages/clay-drop-down/src/DropDown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
*/

import {
FOCUSABLE_ELEMENTS,
FocusScope,
InternalDispatch,
Keys,
Expand Down Expand Up @@ -165,12 +164,6 @@ function ClayDropDown({
value: active,
});

const handleKeyUp = (event: React.KeyboardEvent<HTMLElement>) => {
if (event.key === Keys.Esc) {
setInternalActive(!internalActive);
}
};

useEffect(() => {
if (internalActive) {
const onFocus = (event: FocusEvent) => {
Expand Down Expand Up @@ -218,113 +211,109 @@ function ClayDropDown({

return (
<FocusScope>
<ContainerElement
{...otherProps}
className={classNames('dropdown', className)}
onKeyUp={handleKeyUp}
>
{React.cloneElement(trigger, {
'aria-controls': ariaControls,
'aria-expanded': internalActive,
'aria-haspopup': 'true',
className: classNames(
'dropdown-toggle',
trigger.props.className
),
onClick: (event: React.MouseEvent<HTMLButtonElement>) => {
if (trigger.props.onClick) {
trigger.props.onClick(event);
}

openMenu(!internalActive);
},
onKeyDown: (
event: React.KeyboardEvent<HTMLButtonElement>
) => {
if (trigger.props.onKeyDown) {
trigger.props.onKeyDown(event);
}

if (event.key === Keys.Spacebar) {
{(focusManager) => (
<ContainerElement
{...otherProps}
className={classNames('dropdown', className)}
>
{React.cloneElement(trigger, {
'aria-controls': ariaControls,
'aria-expanded': internalActive,
'aria-haspopup': 'true',
className: classNames(
'dropdown-toggle',
trigger.props.className
),
onClick: (
event: React.MouseEvent<HTMLButtonElement>
) => {
if (trigger.props.onClick) {
trigger.props.onClick(event);
}

openMenu(!internalActive);
}
},
onKeyDown: (
event: React.KeyboardEvent<HTMLButtonElement>
) => {
if (trigger.props.onKeyDown) {
trigger.props.onKeyDown(event);
}

if (event.key === Keys.Down) {
event.preventDefault();
event.stopPropagation();
if (event.key === Keys.Spacebar) {
openMenu(!internalActive);
}

openMenu(true);
}
if (event.key === Keys.Down) {
event.preventDefault();
event.stopPropagation();

if ([Keys.Spacebar, Keys.Down].includes(event.key)) {
event.preventDefault();
}
},
ref: (node: HTMLButtonElement) => {
if (node) {
openMenu(true);
}

if (
[Keys.Spacebar, Keys.Down].includes(event.key)
) {
event.preventDefault();
}
},
ref: (node: HTMLButtonElement) => {
triggerElementRef.current = node;
// Call the original ref, if any.
const {ref} = trigger;
if (typeof ref === 'function') {
ref(node);
}
}
},
})}

{initialized && (
<Menu
{...menuElementAttrs}
active={internalActive}
alignElementRef={triggerElementRef}
alignmentByViewport={alignmentByViewport}
alignmentPosition={alignmentPosition}
closeOnClickOutside={closeOnClickOutside}
hasLeftSymbols={hasLeftSymbols}
hasRightSymbols={hasRightSymbols}
height={menuHeight}
id={ariaControls}
offsetFn={offsetFn}
onSetActive={setInternalActive}
ref={menuElementRef}
width={menuWidth}
>
<FocusMenu
condition={internalActive}
onRender={() => {
// After a few milliseconds querying the elements in the DOM
// inside the menu. This especially when the menu is not
// rendered yet only after the menu is opened, React needs
// to commit the changes to the DOM so that the elements are
// visible and we can move the focus.
setTimeout(() => {
const first =
menuElementRef.current?.querySelector(
// @ts-ignore
FOCUSABLE_ELEMENTS
);

if (first) {
first.focus();
}
}, 10);
}}
},
})}

{initialized && (
<Menu
{...menuElementAttrs}
active={internalActive}
alignElementRef={triggerElementRef}
alignmentByViewport={alignmentByViewport}
alignmentPosition={alignmentPosition}
closeOnClickOutside={closeOnClickOutside}
focusRefOnEsc={triggerElementRef}
hasLeftSymbols={hasLeftSymbols}
hasRightSymbols={hasRightSymbols}
height={menuHeight}
id={ariaControls}
offsetFn={offsetFn}
onActiveChange={setInternalActive}
ref={menuElementRef}
width={menuWidth}
>
<DropDownContext.Provider
value={{
close: () => {
setInternalActive(false);
triggerElementRef.current?.focus();
},
closeOnClick,
<FocusMenu
condition={internalActive}
onRender={() => {
// After a few milliseconds querying the elements in the DOM
// inside the menu. This especially when the menu is not
// rendered yet only after the menu is opened, React needs
// to commit the changes to the DOM so that the elements are
// visible and we can move the focus.
setTimeout(() => {
focusManager.focusNext();
}, 10);
}}
>
{children}
</DropDownContext.Provider>
</FocusMenu>
</Menu>
)}
</ContainerElement>
<DropDownContext.Provider
value={{
close: () => {
setInternalActive(false);
triggerElementRef.current?.focus();
},
closeOnClick,
}}
>
{children}
</DropDownContext.Provider>
</FocusMenu>
</Menu>
)}
</ContainerElement>
)}
</FocusScope>
);
}
Expand Down
17 changes: 13 additions & 4 deletions packages/clay-drop-down/src/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ const ClayDropDownMenu = React.forwardRef<HTMLDivElement, IProps>(
) => {
const setActive = onActiveChange ?? onSetActive;

const menuRef = useRef<HTMLDivElement | null>(null);
const subPortalRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
Expand Down Expand Up @@ -286,7 +287,7 @@ const ClayDropDownMenu = React.forwardRef<HTMLDivElement, IProps>(
);
}

if ((ref as React.RefObject<HTMLElement>).current) {
if (menuRef.current) {
doAlign({
offset: offsetFn(points),
overflow: {
Expand All @@ -295,8 +296,7 @@ const ClayDropDownMenu = React.forwardRef<HTMLDivElement, IProps>(
alwaysByViewport: alignmentByViewport,
},
points,
sourceElement: (ref as React.RefObject<HTMLElement>)
.current!,
sourceElement: menuRef.current,
targetElement: alignElementRef.current,
});
}
Expand Down Expand Up @@ -329,7 +329,16 @@ const ClayDropDownMenu = React.forwardRef<HTMLDivElement, IProps>(
[`dropdown-menu-width-${width}`]: width,
show: active,
})}
ref={ref}
ref={(node) => {
menuRef.current = node;

if (ref instanceof Function) {
ref(node);
} else if (ref instanceof Object) {
// @ts-ignore
ref.current = node;
}
}}
role="presentation"
>
{children}
Expand Down
17 changes: 8 additions & 9 deletions packages/clay-shared/src/FocusScope.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,14 @@ export const FocusScope = ({
onKeyDown(event);
},
ref: (r: HTMLElement) => {
if (r) {
elRef.current = r;
const {ref} = child;
if (ref) {
if (typeof ref === 'object') {
ref.current = r;
} else if (typeof ref === 'function') {
ref(r);
}
elRef.current = r;
const {ref} = child;

if (ref) {
if (typeof ref === 'object') {
ref.current = r;
} else if (typeof ref === 'function') {
ref(r);
}
}
},
Expand Down
2 changes: 1 addition & 1 deletion packages/clay-shared/src/useFocusManagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ const getFiber = (scope: React.RefObject<HTMLElement | null>) => {

const getFocusableElementsInScope = (fiberNode: any) => {
const focusableElements: Array<any> = [];
const {child} = fiberNode;
const {child} = fiberNode.alternate ?? fiberNode;

if (child !== null) {
collectFocusableElements(child, focusableElements);
Expand Down

0 comments on commit 89c87da

Please sign in to comment.