mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2025-12-19 17:58:43 -05:00
fix: add listbox focus border (#1821)
This commit is contained in:
43
apis/nucleus/src/components/listbox/ListBoxFocusBorder.jsx
Normal file
43
apis/nucleus/src/components/listbox/ListBoxFocusBorder.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { styled } from '@mui/material';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
const StyledBorder = styled('div')(({ theme, width, height }) => ({
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 1,
|
||||
width,
|
||||
height,
|
||||
boxShadow: `inset 0 0 0 2px ${theme.palette.custom.focusBorder}`,
|
||||
}));
|
||||
|
||||
export default function ListBoxFocusBorder({ width, height, isModalMode, childNode, containerNode }) {
|
||||
const [isOnlyContainerFocused, setIsOnlyContainerFocused] = useState(false);
|
||||
|
||||
const checkFocus = useCallback(() => {
|
||||
const containerFocused = containerNode && containerNode.contains(document.activeElement);
|
||||
const childFocused = childNode && childNode.contains(document.activeElement);
|
||||
setIsOnlyContainerFocused(containerFocused && !childFocused);
|
||||
}, [containerNode, childNode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerNode) {
|
||||
return undefined;
|
||||
}
|
||||
containerNode.addEventListener('focusin', checkFocus);
|
||||
containerNode.addEventListener('focusout', checkFocus);
|
||||
|
||||
checkFocus();
|
||||
|
||||
return () => {
|
||||
containerNode.removeEventListener('focusin', checkFocus);
|
||||
containerNode.removeEventListener('focusout', checkFocus);
|
||||
};
|
||||
}, [checkFocus, containerNode]);
|
||||
|
||||
const show = !isModalMode && isOnlyContainerFocused;
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <StyledBorder aria-hidden="true" width={width} height={height} />;
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import useRect from '../../hooks/useRect';
|
||||
import isDirectQueryEnabled from './utils/is-direct-query';
|
||||
import getContainerPadding from './assets/list-sizes/container-padding';
|
||||
import ListBoxHeader from './components/ListBoxHeader';
|
||||
import ListBoxFocusBorder from './ListBoxFocusBorder';
|
||||
|
||||
const PREFIX = 'ListBoxInline';
|
||||
const classes = {
|
||||
@@ -28,8 +29,8 @@ const classes = {
|
||||
};
|
||||
|
||||
const StyledGrid = styled(Grid, {
|
||||
shouldForwardProp: (p) => !['containerPadding', 'isGridMode', 'styles'].includes(p),
|
||||
})(({ theme, containerPadding, isGridMode, styles }) => ({
|
||||
shouldForwardProp: (p) => !['containerPadding', 'styles'].includes(p),
|
||||
})(({ containerPadding, styles }) => ({
|
||||
...styles.background, // sets background color and image of listbox
|
||||
[`& .${classes.listBoxHeader}`]: {
|
||||
alignSelf: 'center',
|
||||
@@ -44,12 +45,6 @@ const StyledGrid = styled(Grid, {
|
||||
[`& .${classes.listboxWrapper}`]: {
|
||||
padding: containerPadding,
|
||||
},
|
||||
'&:focus': {
|
||||
boxShadow: `inset 0 0 0 2px ${theme.palette.custom.focusBorder} !important`,
|
||||
},
|
||||
'&:focus ::-webkit-scrollbar-track': {
|
||||
boxShadow: !isGridMode ? 'inset -2px -2px 0px #3F8AB3' : undefined,
|
||||
},
|
||||
'&:focus-visible': {
|
||||
outline: 'none',
|
||||
},
|
||||
@@ -94,7 +89,7 @@ function ListBoxInline({ options, layout }) {
|
||||
|
||||
const containerRef = useRef();
|
||||
const searchInputRef = useRef();
|
||||
const [containerRectRef, containerRect] = useRect();
|
||||
const [containerRectRef, containerRect, containerNode] = useRect();
|
||||
const [showToolbar, setShowToolbar] = useState(false);
|
||||
const [showSearch, setShowSearch] = useState(false);
|
||||
const hovering = useRef(false);
|
||||
@@ -107,6 +102,11 @@ function ListBoxInline({ options, layout }) {
|
||||
const isModalMode = useCallback(() => isModal({ app, appSelections }), [app, appSelections]);
|
||||
const isInvalid = layout?.qListObject.qDimensionInfo.qError;
|
||||
const errorText = isInvalid && constraints.active ? 'Visualization.Invalid.Dimension' : 'Visualization.Incomplete';
|
||||
const [, setHasFocus] = useState(false); // Force render on focus change to show/hide ListBoxFocusBorder
|
||||
const [listboxChildNode, setListboxChildNode] = useState(null);
|
||||
const listboxChildRef = useCallback((node) => {
|
||||
setListboxChildNode(node);
|
||||
}, []);
|
||||
|
||||
const { handleKeyDown, handleOnMouseEnter, handleOnMouseLeave, globalKeyDown } = useMemo(
|
||||
() =>
|
||||
@@ -272,11 +272,19 @@ function ListBoxInline({ options, layout }) {
|
||||
onMouseLeave={handleOnMouseLeave}
|
||||
ref={(el) => {
|
||||
containerRef.current = el;
|
||||
containerRectRef(el);
|
||||
containerRectRef?.(el);
|
||||
}}
|
||||
isGridMode={isGridMode}
|
||||
aria-label={keyboard.active ? translator.get('Listbox.ScreenReaderInstructions') : ''}
|
||||
onFocus={() => setHasFocus(true)}
|
||||
onBlur={() => setHasFocus(false)}
|
||||
>
|
||||
<ListBoxFocusBorder
|
||||
width={containerRect?.width}
|
||||
height={containerRect?.height}
|
||||
isModalMode={isModalMode()}
|
||||
childNode={listboxChildNode}
|
||||
containerNode={containerNode}
|
||||
/>
|
||||
{showAttachedToolbar && listBoxHeader}
|
||||
<Grid
|
||||
item
|
||||
@@ -286,6 +294,7 @@ function ListBoxInline({ options, layout }) {
|
||||
minHeight={listBoxMinHeight}
|
||||
role="region"
|
||||
aria-label={translator.get('Listbox.ResultFilterLabel')}
|
||||
ref={listboxChildRef}
|
||||
>
|
||||
<Grid item>
|
||||
<ListBoxSearch
|
||||
|
||||
@@ -16,6 +16,7 @@ import * as ListBoxSearchModule from '../components/ListBoxSearch';
|
||||
import * as listboxSelectionToolbarModule from '../interactions/listbox-selection-toolbar';
|
||||
import * as styling from '../assets/styling';
|
||||
import * as isDirectQueryEnabled from '../utils/is-direct-query';
|
||||
import * as useAppSelection from '../../../hooks/useAppSelections';
|
||||
|
||||
const virtualizedModule = require('react-virtualized-auto-sizer');
|
||||
const listboxKeyboardNavigationModule = require('../interactions/keyboard-navigation/keyboard-nav-container');
|
||||
@@ -94,6 +95,7 @@ describe('<ListboxInline />', () => {
|
||||
.spyOn(styling, 'default')
|
||||
.mockImplementation(() => ({ backgroundColor: '#FFFFFF', header: {}, content: {}, selections: {} }));
|
||||
jest.spyOn(isDirectQueryEnabled, 'default').mockImplementation(() => false);
|
||||
jest.spyOn(useAppSelection, 'default').mockImplementation(() => [{ isInModal: jest.fn().mockReturnValue(false) }]);
|
||||
|
||||
ActionsToolbarModule.default = ActionsToolbar;
|
||||
ListBoxModule.default = <div className="theListBox" />;
|
||||
@@ -188,7 +190,7 @@ describe('<ListboxInline />', () => {
|
||||
expect(ListBoxSearch.mock.calls[0][0]).toMatchObject({
|
||||
visible: true,
|
||||
});
|
||||
expect(getListboxInlineKeyboardNavigation).toHaveBeenCalledTimes(2);
|
||||
expect(getListboxInlineKeyboardNavigation).toHaveBeenCalledTimes(3);
|
||||
|
||||
// TODO: MUIv5
|
||||
// expect(renderer.toJSON().props.onKeyDown).toBe('keyboard-navigation');
|
||||
|
||||
Reference in New Issue
Block a user