mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2025-12-21 02:37:20 -05:00
feat(ListBox): break out header and add unlock button (#1466)
* fix: error on esc in search * fix: reset zoom on iphone on nebula level * fix: show toolbar when icons demand * feat: unlock cover btn and break out header * fix: move option * feat: move showLock option to public options * fix: centered svg unlock icon * fix: listen to lock changes * fix: search icon show fixes * fix: hide header correctly * fix: make text overflow great again * test: updated expected snapshots * fix: tweak icon sizes more * fix: apply header style color * test: update snapshots * fix: override text explicitly to make it work * fix: rtl actions keyboard nav inverted * fix: keyboard nav for unlock button * fix: align numerical search results correctly * fix: recreate header on layout or isLocked change * fix: give layout to can-funcs to stay up to date * Update apis/nucleus/src/components/listbox/ListBoxInline.jsx Co-authored-by: Daniel Sjöstrand <99665802+DanielS-Qlik@users.noreply.github.com> * refactor: add loading icon and block interaction * fix: justify center when loading * fix: adjust spinner timeout * chore: update specs * feat: moved translation strings --------- Co-authored-by: Daniel Sjöstrand <99665802+DanielS-Qlik@users.noreply.github.com>
This commit is contained in:
@@ -258,5 +258,10 @@
|
|||||||
"value": "Die Berechnungsbedingung ist nicht erfüllt",
|
"value": "Die Berechnungsbedingung ist nicht erfüllt",
|
||||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||||
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
||||||
|
},
|
||||||
|
"SelectionToolbar.ClickToLock": {
|
||||||
|
"value": "Klicken zum Sperren",
|
||||||
|
"comment": "Lock a selection in selection toolbar",
|
||||||
|
"version": "yOnFI2n2tK+87jTmGHU5vw=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,6 +179,10 @@
|
|||||||
"value": "Click to unlock",
|
"value": "Click to unlock",
|
||||||
"comment": "Unlock a selection from listbox"
|
"comment": "Unlock a selection from listbox"
|
||||||
},
|
},
|
||||||
|
"SelectionToolbar.ClickToLock": {
|
||||||
|
"value": "Click to lock",
|
||||||
|
"comment": "Lock a selection in selections toolbar"
|
||||||
|
},
|
||||||
"Visualization.LayoutError": {
|
"Visualization.LayoutError": {
|
||||||
"value": "Error",
|
"value": "Error",
|
||||||
"comment": "Status text shown when a visualization has layout errors"
|
"comment": "Status text shown when a visualization has layout errors"
|
||||||
|
|||||||
@@ -258,5 +258,10 @@
|
|||||||
"value": "La condición de cálculo no se cumple",
|
"value": "La condición de cálculo no se cumple",
|
||||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||||
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
||||||
|
},
|
||||||
|
"SelectionToolbar.ClickToLock": {
|
||||||
|
"value": "Haga clic para bloquearla",
|
||||||
|
"comment": "Lock a selection in selection toolbar",
|
||||||
|
"version": "yOnFI2n2tK+87jTmGHU5vw=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,5 +258,10 @@
|
|||||||
"value": "Condition de calcul non remplie",
|
"value": "Condition de calcul non remplie",
|
||||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||||
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
||||||
|
},
|
||||||
|
"SelectionToolbar.ClickToLock": {
|
||||||
|
"value": "Cliquez pour verrouiller",
|
||||||
|
"comment": "Lock a selection in selection toolbar",
|
||||||
|
"version": "yOnFI2n2tK+87jTmGHU5vw=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,5 +258,10 @@
|
|||||||
"value": "La condizione di calcolo non è soddisfatta",
|
"value": "La condizione di calcolo non è soddisfatta",
|
||||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||||
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
||||||
|
},
|
||||||
|
"SelectionToolbar.ClickToLock": {
|
||||||
|
"value": "Fare clic per bloccare",
|
||||||
|
"comment": "Lock a selection in selection toolbar",
|
||||||
|
"version": "yOnFI2n2tK+87jTmGHU5vw=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,5 +258,10 @@
|
|||||||
"value": "演算実行条件が満たされていません",
|
"value": "演算実行条件が満たされていません",
|
||||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||||
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
||||||
|
},
|
||||||
|
"SelectionToolbar.ClickToLock": {
|
||||||
|
"value": "クリックしてロック",
|
||||||
|
"comment": "Lock a selection in selection toolbar",
|
||||||
|
"version": "yOnFI2n2tK+87jTmGHU5vw=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,5 +258,10 @@
|
|||||||
"value": "계산 조건이 충족되지 않았습니다.",
|
"value": "계산 조건이 충족되지 않았습니다.",
|
||||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||||
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
||||||
|
},
|
||||||
|
"SelectionToolbar.ClickToLock": {
|
||||||
|
"value": "잠그려면 클릭하십시오.",
|
||||||
|
"comment": "Lock a selection in selection toolbar",
|
||||||
|
"version": "yOnFI2n2tK+87jTmGHU5vw=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,5 +258,10 @@
|
|||||||
"value": "Er is niet aan de berekeningsvoorwaarde voldaan",
|
"value": "Er is niet aan de berekeningsvoorwaarde voldaan",
|
||||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||||
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
||||||
|
},
|
||||||
|
"SelectionToolbar.ClickToLock": {
|
||||||
|
"value": "Klik om te vergrendelen",
|
||||||
|
"comment": "Lock a selection in selection toolbar",
|
||||||
|
"version": "yOnFI2n2tK+87jTmGHU5vw=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,5 +258,10 @@
|
|||||||
"value": "Warunek obliczenia nie jest spełniony",
|
"value": "Warunek obliczenia nie jest spełniony",
|
||||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||||
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
||||||
|
},
|
||||||
|
"SelectionToolbar.ClickToLock": {
|
||||||
|
"value": "Aby zablokować, kliknij",
|
||||||
|
"comment": "Lock a selection in selection toolbar",
|
||||||
|
"version": "yOnFI2n2tK+87jTmGHU5vw=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,5 +258,10 @@
|
|||||||
"value": "A condição de cálculo não foi atendida",
|
"value": "A condição de cálculo não foi atendida",
|
||||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||||
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
||||||
|
},
|
||||||
|
"SelectionToolbar.ClickToLock": {
|
||||||
|
"value": "Clique para travar",
|
||||||
|
"comment": "Lock a selection in selection toolbar",
|
||||||
|
"version": "yOnFI2n2tK+87jTmGHU5vw=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,5 +258,10 @@
|
|||||||
"value": "Условие вычисления не выполнено",
|
"value": "Условие вычисления не выполнено",
|
||||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||||
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
||||||
|
},
|
||||||
|
"SelectionToolbar.ClickToLock": {
|
||||||
|
"value": "Щелкните, чтобы заблокировать",
|
||||||
|
"comment": "Lock a selection in selection toolbar",
|
||||||
|
"version": "yOnFI2n2tK+87jTmGHU5vw=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,5 +258,10 @@
|
|||||||
"value": "Beräkningsvillkoret uppfylls inte",
|
"value": "Beräkningsvillkoret uppfylls inte",
|
||||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||||
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
||||||
|
},
|
||||||
|
"SelectionToolbar.ClickToLock": {
|
||||||
|
"value": "Klicka för att låsa",
|
||||||
|
"comment": "Lock a selection in selection toolbar",
|
||||||
|
"version": "yOnFI2n2tK+87jTmGHU5vw=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,5 +258,10 @@
|
|||||||
"value": "Hesaplama koşulu yerine getirilmedi",
|
"value": "Hesaplama koşulu yerine getirilmedi",
|
||||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||||
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
||||||
|
},
|
||||||
|
"SelectionToolbar.ClickToLock": {
|
||||||
|
"value": "Kilitlemek için tıklayın",
|
||||||
|
"comment": "Lock a selection in selection toolbar",
|
||||||
|
"version": "yOnFI2n2tK+87jTmGHU5vw=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,5 +258,10 @@
|
|||||||
"value": "不满足计算条件",
|
"value": "不满足计算条件",
|
||||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||||
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
||||||
|
},
|
||||||
|
"SelectionToolbar.ClickToLock": {
|
||||||
|
"value": "单击以锁定",
|
||||||
|
"comment": "Lock a selection in selection toolbar",
|
||||||
|
"version": "yOnFI2n2tK+87jTmGHU5vw=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,5 +258,10 @@
|
|||||||
"value": "不符計算條件",
|
"value": "不符計算條件",
|
||||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||||
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
||||||
|
},
|
||||||
|
"SelectionToolbar.ClickToLock": {
|
||||||
|
"value": "按一下鎖定",
|
||||||
|
"comment": "Lock a selection in selection toolbar",
|
||||||
|
"version": "yOnFI2n2tK+87jTmGHU5vw=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ function ActionsToolbar({
|
|||||||
onConfirm: () => {},
|
onConfirm: () => {},
|
||||||
onCancel: () => {},
|
onCancel: () => {},
|
||||||
},
|
},
|
||||||
|
extraItems,
|
||||||
more = {
|
more = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
actions: [],
|
actions: [],
|
||||||
@@ -78,10 +79,11 @@ function ActionsToolbar({
|
|||||||
},
|
},
|
||||||
focusHandler = null,
|
focusHandler = null,
|
||||||
actionsRefMock = null, // for testing
|
actionsRefMock = null, // for testing
|
||||||
direction = 'ltr',
|
isRtl,
|
||||||
autoConfirm = false,
|
autoConfirm = false,
|
||||||
|
layout,
|
||||||
}) {
|
}) {
|
||||||
const defaultSelectionActions = useDefaultSelectionActions(selections);
|
const defaultSelectionActions = useDefaultSelectionActions({ ...selections, layout });
|
||||||
|
|
||||||
const { translator, keyboardNavigation } = useContext(InstanceContext);
|
const { translator, keyboardNavigation } = useContext(InstanceContext);
|
||||||
const [showMoreItems, setShowMoreItems] = useState(false);
|
const [showMoreItems, setShowMoreItems] = useState(false);
|
||||||
@@ -104,8 +106,8 @@ function ActionsToolbar({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleActionsKeyDown = useMemo(
|
const handleActionsKeyDown = useMemo(
|
||||||
() => getActionsKeyDownHandler({ keyboardNavigation, focusHandler, getEnabledButton, selections }),
|
() => getActionsKeyDownHandler({ keyboardNavigation, focusHandler, getEnabledButton, selections, isRtl }),
|
||||||
[keyboardNavigation, focusHandler, getEnabledButton, selections]
|
[keyboardNavigation, focusHandler, getEnabledButton, selections, isRtl]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
@@ -120,11 +122,11 @@ function ActionsToolbar({
|
|||||||
|
|
||||||
const focusFirst = () => {
|
const focusFirst = () => {
|
||||||
const enabledButton = getEnabledButton(false);
|
const enabledButton = getEnabledButton(false);
|
||||||
enabledButton && enabledButton.focus();
|
enabledButton?.focus();
|
||||||
};
|
};
|
||||||
const focusLast = () => {
|
const focusLast = () => {
|
||||||
const enabledButton = getEnabledButton(true);
|
const enabledButton = getEnabledButton(true);
|
||||||
enabledButton && enabledButton.focus();
|
enabledButton?.focus();
|
||||||
};
|
};
|
||||||
focusHandler.on('focus_toolbar_first', focusFirst);
|
focusHandler.on('focus_toolbar_first', focusFirst);
|
||||||
focusHandler.on('focus_toolbar_last', focusLast);
|
focusHandler.on('focus_toolbar_last', focusLast);
|
||||||
@@ -163,7 +165,6 @@ function ActionsToolbar({
|
|||||||
const showActions = newActions.length > 0;
|
const showActions = newActions.length > 0;
|
||||||
const showMore = moreActions.length > 0;
|
const showMore = moreActions.length > 0;
|
||||||
const showDivider = (showActions && selections.show) || (showMore && selections.show);
|
const showDivider = (showActions && selections.show) || (showMore && selections.show);
|
||||||
const isRtl = direction === 'rtl';
|
|
||||||
|
|
||||||
const Actions = (
|
const Actions = (
|
||||||
<Grid
|
<Grid
|
||||||
@@ -176,6 +177,9 @@ function ActionsToolbar({
|
|||||||
data-testid="actions-toolbar"
|
data-testid="actions-toolbar"
|
||||||
sx={{ flexDirection: isRtl ? 'row-reverse' : 'row' }}
|
sx={{ flexDirection: isRtl ? 'row-reverse' : 'row' }}
|
||||||
>
|
>
|
||||||
|
{extraItems?.length && (
|
||||||
|
<ActionsGroup className="actions-toolbar-extra-actions" actions={extraItems} isRtl={isRtl} />
|
||||||
|
)}
|
||||||
{showActions && <ActionsGroup actions={newActions} />}
|
{showActions && <ActionsGroup actions={newActions} />}
|
||||||
{showMore && (
|
{showMore && (
|
||||||
<ActionsGroup
|
<ActionsGroup
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ function Header({ layout, sn, anchorEl, hovering, focusHandler, titleStyles = {}
|
|||||||
actions={actions}
|
actions={actions}
|
||||||
popover={{ show: showPopoverToolbar, anchorEl }}
|
popover={{ show: showPopoverToolbar, anchorEl }}
|
||||||
focusHandler={focusHandler}
|
focusHandler={focusHandler}
|
||||||
|
layout={layout}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,13 @@ const focusButton = (index) => {
|
|||||||
btn.focus();
|
btn.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function getActionsKeyDownHandler({ keyboardNavigation, focusHandler, getEnabledButton, selections }) {
|
export default function getActionsKeyDownHandler({
|
||||||
|
keyboardNavigation,
|
||||||
|
focusHandler,
|
||||||
|
getEnabledButton,
|
||||||
|
selections,
|
||||||
|
isRtl,
|
||||||
|
}) {
|
||||||
const handleActionsKeyDown = (evt) => {
|
const handleActionsKeyDown = (evt) => {
|
||||||
const { target, nativeEvent } = evt;
|
const { target, nativeEvent } = evt;
|
||||||
const { keyCode } = nativeEvent;
|
const { keyCode } = nativeEvent;
|
||||||
@@ -29,7 +35,8 @@ export default function getActionsKeyDownHandler({ keyboardNavigation, focusHand
|
|||||||
const isActionButton = target.classList.contains('njs-cell-action');
|
const isActionButton = target.classList.contains('njs-cell-action');
|
||||||
if (isActionButton) {
|
if (isActionButton) {
|
||||||
const index = getActionButtonIndex(target);
|
const index = getActionButtonIndex(target);
|
||||||
const pressedLeft = [KEYS.ARROW_LEFT, KEYS.ARROW_DOWN].includes(keyCode);
|
let pressedLeft = [KEYS.ARROW_LEFT, KEYS.ARROW_DOWN].includes(keyCode);
|
||||||
|
pressedLeft = isRtl ? !pressedLeft : pressedLeft; // invert direction when using RTL
|
||||||
focusButton(pressedLeft ? index - 1 : index + 1);
|
focusButton(pressedLeft ? index - 1 : index + 1);
|
||||||
}
|
}
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
|
|||||||
@@ -2,37 +2,23 @@
|
|||||||
import React, { useContext, useCallback, useRef, useEffect, useState, useMemo } from 'react';
|
import React, { useContext, useCallback, useRef, useEffect, useState, useMemo } from 'react';
|
||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import Lock from '@nebula.js/ui/icons/lock';
|
import { Grid } from '@mui/material';
|
||||||
import { IconButton, Grid, Typography } from '@mui/material';
|
|
||||||
import { useTheme } from '@nebula.js/ui/theme';
|
import { useTheme } from '@nebula.js/ui/theme';
|
||||||
import SearchIcon from '@nebula.js/ui/icons/search';
|
|
||||||
import DrillDownIcon from '@nebula.js/ui/icons/drill-down';
|
|
||||||
import useLayout from '../../hooks/useLayout';
|
import useLayout from '../../hooks/useLayout';
|
||||||
import ListBox from './ListBox';
|
import ListBox from './ListBox';
|
||||||
import createListboxSelectionToolbar from './interactions/listbox-selection-toolbar';
|
|
||||||
import ActionsToolbar from '../ActionsToolbar';
|
|
||||||
import InstanceContext from '../../contexts/InstanceContext';
|
import InstanceContext from '../../contexts/InstanceContext';
|
||||||
import ListBoxSearch from './components/ListBoxSearch';
|
import ListBoxSearch from './components/ListBoxSearch';
|
||||||
import getListboxContainerKeyboardNavigation from './interactions/keyboard-navigation/keyboard-nav-container';
|
import getListboxContainerKeyboardNavigation from './interactions/keyboard-navigation/keyboard-nav-container';
|
||||||
import getStyles from './assets/styling';
|
import getStyles from './assets/styling';
|
||||||
import useAppSelections from '../../hooks/useAppSelections';
|
import useAppSelections from '../../hooks/useAppSelections';
|
||||||
import showToolbarDetached from './interactions/listbox-show-toolbar-detached';
|
|
||||||
import getListboxActionProps from './interactions/listbox-get-action-props';
|
|
||||||
import createSelectionState from './hooks/selections/selectionState';
|
import createSelectionState from './hooks/selections/selectionState';
|
||||||
import {
|
import { DENSE_ROW_HEIGHT, SCROLL_BAR_WIDTH } from './constants';
|
||||||
CELL_PADDING_LEFT,
|
|
||||||
ICON_WIDTH,
|
|
||||||
ICON_PADDING,
|
|
||||||
BUTTON_ICON_WIDTH,
|
|
||||||
HEADER_PADDING_RIGHT,
|
|
||||||
DENSE_ROW_HEIGHT,
|
|
||||||
SCROLL_BAR_WIDTH,
|
|
||||||
} from './constants';
|
|
||||||
import useTempKeyboard from './components/useTempKeyboard';
|
import useTempKeyboard from './components/useTempKeyboard';
|
||||||
import ListBoxError from './components/ListBoxError';
|
import ListBoxError from './components/ListBoxError';
|
||||||
import useRect from '../../hooks/useRect';
|
import useRect from '../../hooks/useRect';
|
||||||
import isDirectQueryEnabled from './utils/is-direct-query';
|
import isDirectQueryEnabled from './utils/is-direct-query';
|
||||||
import getContainerPadding from './assets/list-sizes/container-padding';
|
import getContainerPadding from './assets/list-sizes/container-padding';
|
||||||
|
import ListBoxHeader from './components/ListBoxHeader';
|
||||||
|
|
||||||
const PREFIX = 'ListBoxInline';
|
const PREFIX = 'ListBoxInline';
|
||||||
const classes = {
|
const classes = {
|
||||||
@@ -69,25 +55,6 @@ const StyledGrid = styled(Grid, {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledGridHeader = styled(Grid, { shouldForwardProp: (p) => !['styles', 'isRtl'].includes(p) })(
|
|
||||||
({ styles, isRtl }) => ({
|
|
||||||
flexDirection: isRtl ? 'row-reverse' : 'row',
|
|
||||||
wrap: 'nowrap',
|
|
||||||
minHeight: 32,
|
|
||||||
alignContent: 'center',
|
|
||||||
...styles.header,
|
|
||||||
'& *': {
|
|
||||||
// Assign the styles color as defaul color for all elements in the header
|
|
||||||
color: styles.header.color,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const Title = styled(Typography)(({ styles }) => ({
|
|
||||||
...styles.header,
|
|
||||||
paddingRight: '1px', // make place for italic font style
|
|
||||||
}));
|
|
||||||
|
|
||||||
const isModal = ({ app, appSelections }) => app.isInModalSelection?.() ?? appSelections.isInModal();
|
const isModal = ({ app, appSelections }) => app.isInModalSelection?.() ?? appSelections.isInModal();
|
||||||
|
|
||||||
function ListBoxInline({ options, layout }) {
|
function ListBoxInline({ options, layout }) {
|
||||||
@@ -111,6 +78,7 @@ function ListBoxInline({ options, layout }) {
|
|||||||
renderedCallback,
|
renderedCallback,
|
||||||
toolbar = true,
|
toolbar = true,
|
||||||
isPopover = false,
|
isPopover = false,
|
||||||
|
showLock = false,
|
||||||
components,
|
components,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
@@ -130,10 +98,6 @@ function ListBoxInline({ options, layout }) {
|
|||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const unlock = useCallback(() => {
|
|
||||||
model.unlock('/qListObjectDef');
|
|
||||||
}, [model]);
|
|
||||||
|
|
||||||
const { translator, keyboardNavigation, themeApi, constraints } = useContext(InstanceContext);
|
const { translator, keyboardNavigation, themeApi, constraints } = useContext(InstanceContext);
|
||||||
|
|
||||||
const { checkboxes = checkboxesOption } = layout || {};
|
const { checkboxes = checkboxesOption } = layout || {};
|
||||||
@@ -152,13 +116,11 @@ function ListBoxInline({ options, layout }) {
|
|||||||
const updateKeyScroll = (newState) => setKeyScroll((current) => ({ ...current, ...newState }));
|
const updateKeyScroll = (newState) => setKeyScroll((current) => ({ ...current, ...newState }));
|
||||||
const [currentScrollIndex, setCurrentScrollIndex] = useState({ start: 0, stop: 0 });
|
const [currentScrollIndex, setCurrentScrollIndex] = useState({ start: 0, stop: 0 });
|
||||||
const [appSelections] = useAppSelections(app);
|
const [appSelections] = useAppSelections(app);
|
||||||
const titleRef = useRef(null);
|
|
||||||
const [selectionState] = useState(() => createSelectionState());
|
const [selectionState] = useState(() => createSelectionState());
|
||||||
const keyboard = useTempKeyboard({ containerRef, enabled: keyboardNavigation });
|
const keyboard = useTempKeyboard({ containerRef, enabled: keyboardNavigation });
|
||||||
const isModalMode = useCallback(() => isModal({ app, appSelections }), [app, appSelections]);
|
const isModalMode = useCallback(() => isModal({ app, appSelections }), [app, appSelections]);
|
||||||
const isInvalid = layout?.qListObject.qDimensionInfo.qError;
|
const isInvalid = layout?.qListObject.qDimensionInfo.qError;
|
||||||
const errorText = isInvalid && constraints.active ? 'Visualization.Invalid.Dimension' : 'Visualization.Incomplete';
|
const errorText = isInvalid && constraints.active ? 'Visualization.Invalid.Dimension' : 'Visualization.Incomplete';
|
||||||
const [isToolbarDetached, setIsToolbarDetached] = useState(false);
|
|
||||||
|
|
||||||
const { handleKeyDown, handleOnMouseEnter, handleOnMouseLeave, globalKeyDown } = useMemo(
|
const { handleKeyDown, handleOnMouseEnter, handleOnMouseLeave, globalKeyDown } = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -186,9 +148,6 @@ function ListBoxInline({ options, layout }) {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const showDetachedToolbarOnly = toolbar && (layout?.title === '' || layout?.showTitle === false) && !isPopover;
|
|
||||||
const showToolbarWithTitle = (toolbar && layout?.title !== '' && layout?.showTitle !== false) || isPopover;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.addEventListener('keydown', globalKeyDown);
|
document.addEventListener('keydown', globalKeyDown);
|
||||||
return () => {
|
return () => {
|
||||||
@@ -197,6 +156,9 @@ function ListBoxInline({ options, layout }) {
|
|||||||
}, [globalKeyDown]);
|
}, [globalKeyDown]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (search === true) {
|
||||||
|
setShowSearch(true);
|
||||||
|
}
|
||||||
const show = () => {
|
const show = () => {
|
||||||
setShowToolbar(true);
|
setShowToolbar(true);
|
||||||
};
|
};
|
||||||
@@ -207,6 +169,7 @@ function ListBoxInline({ options, layout }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (isPopover) {
|
if (isPopover) {
|
||||||
|
// When isPopover, toolbar == false will be ignored.
|
||||||
if (!selections.isActive()) {
|
if (!selections.isActive()) {
|
||||||
selections.begin('/qListObjectDef');
|
selections.begin('/qListObjectDef');
|
||||||
selections.on('activated', show);
|
selections.on('activated', show);
|
||||||
@@ -214,7 +177,7 @@ function ListBoxInline({ options, layout }) {
|
|||||||
}
|
}
|
||||||
setShowToolbar(isPopover);
|
setShowToolbar(isPopover);
|
||||||
}
|
}
|
||||||
if (selections) {
|
if (toolbar && selections) {
|
||||||
if (!selections.isModal()) {
|
if (!selections.isModal()) {
|
||||||
selections.on('activated', show);
|
selections.on('activated', show);
|
||||||
selections.on('deactivated', hide);
|
selections.on('deactivated', hide);
|
||||||
@@ -228,7 +191,7 @@ function ListBoxInline({ options, layout }) {
|
|||||||
selections.removeListener('deactivated', hide);
|
selections.removeListener('deactivated', hide);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [selections, isPopover]);
|
}, [toolbar, selections, isPopover]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!searchContainer || !searchContainer.current) {
|
if (!searchContainer || !searchContainer.current) {
|
||||||
@@ -242,75 +205,38 @@ function ListBoxInline({ options, layout }) {
|
|||||||
}, [searchContainer && searchContainer.current, showSearch, search, focusSearch]);
|
}, [searchContainer && searchContainer.current, showSearch, search, focusSearch]);
|
||||||
|
|
||||||
const { wildCardSearch, searchEnabled, autoConfirm = false, layoutOptions = {} } = layout ?? {};
|
const { wildCardSearch, searchEnabled, autoConfirm = false, layoutOptions = {} } = layout ?? {};
|
||||||
const showSearchIcon = searchEnabled !== false && search === 'toggle';
|
const isLocked = layout?.qListObject?.qDimensionInfo?.qLocked;
|
||||||
const isLocked = layout?.qListObject?.qDimensionInfo?.qLocked === true;
|
const showSearchIcon = searchEnabled !== false && search === 'toggle' && !isLocked;
|
||||||
const showSearchOrLockIcon = isLocked || showSearchIcon;
|
|
||||||
const isDrillDown = layout?.qListObject?.qDimensionInfo?.qGrouping === 'H';
|
const isDrillDown = layout?.qListObject?.qDimensionInfo?.qGrouping === 'H';
|
||||||
const showIcons = showSearchOrLockIcon || isDrillDown;
|
|
||||||
const iconsWidth = (showSearchOrLockIcon ? BUTTON_ICON_WIDTH : 0) + (isDrillDown ? ICON_WIDTH + ICON_PADDING : 0); // Drill-down icon needs padding right so there is space between the icon and the title
|
const canShowTitle = layout?.title?.length && layout?.showTitle !== false;
|
||||||
|
const showDetachedToolbarOnly = toolbar && !canShowTitle && !isPopover;
|
||||||
|
const showAttachedToolbar = (toolbar && canShowTitle) || isPopover;
|
||||||
|
|
||||||
const isRtl = direction === 'rtl';
|
const isRtl = direction === 'rtl';
|
||||||
const headerPaddingLeft = CELL_PADDING_LEFT - (showSearchOrLockIcon ? ICON_PADDING : 0);
|
|
||||||
const headerPaddingRight = isRtl ? CELL_PADDING_LEFT - (showIcons ? ICON_PADDING : 0) : HEADER_PADDING_RIGHT;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!titleRef.current || !containerRect) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isDetached = showToolbarDetached({
|
|
||||||
containerRect,
|
|
||||||
titleRef,
|
|
||||||
iconsWidth,
|
|
||||||
headerPaddingLeft,
|
|
||||||
headerPaddingRight,
|
|
||||||
});
|
|
||||||
setIsToolbarDetached(isDetached);
|
|
||||||
}, [titleRef.current, containerRect]);
|
|
||||||
|
|
||||||
if (!model || !layout || !translator) {
|
if (!model || !layout || !translator) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const listboxSelectionToolbarItems = createListboxSelectionToolbar({
|
|
||||||
layout,
|
|
||||||
model,
|
|
||||||
translator,
|
|
||||||
selectionState,
|
|
||||||
isDirectQuery,
|
|
||||||
});
|
|
||||||
|
|
||||||
const showTitle = true;
|
|
||||||
const showSearchToggle = search === 'toggle' && showSearch;
|
const showSearchToggle = search === 'toggle' && showSearch;
|
||||||
const searchVisible = (search === true || showSearchToggle) && !selectDisabled() && searchEnabled !== false;
|
const searchVisible = search === true || (showSearchToggle && !selectDisabled() && searchEnabled !== false);
|
||||||
const dense = layoutOptions.dense ?? false;
|
const dense = layoutOptions.dense ?? false;
|
||||||
|
|
||||||
const onShowSearch = () => {
|
const handleShowSearch = () => {
|
||||||
const newValue = !showSearch;
|
const newValue = !showSearch;
|
||||||
setShowSearch(newValue);
|
setShowSearch(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCtrlF = () => {
|
const onCtrlF = () => {
|
||||||
if (search === 'toggle') {
|
if (search === 'toggle') {
|
||||||
onShowSearch();
|
handleShowSearch();
|
||||||
} else {
|
} else {
|
||||||
const input = searchContainer.current.querySelector('input');
|
const input = searchContainer.current.querySelector('input');
|
||||||
input?.focus();
|
input?.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getActionToolbarProps = (isDetached) => ({
|
|
||||||
...getListboxActionProps({
|
|
||||||
isDetached: isPopover ? false : isDetached,
|
|
||||||
showToolbar,
|
|
||||||
containerRef,
|
|
||||||
isLocked,
|
|
||||||
listboxSelectionToolbarItems,
|
|
||||||
selections,
|
|
||||||
keyboard,
|
|
||||||
}),
|
|
||||||
autoConfirm,
|
|
||||||
});
|
|
||||||
|
|
||||||
const shouldAutoFocus = searchVisible && search === 'toggle';
|
const shouldAutoFocus = searchVisible && search === 'toggle';
|
||||||
|
|
||||||
// Add a container padding for grid mode to harmonize with the grid item margins (should sum to 8px).
|
// Add a container padding for grid mode to harmonize with the grid item margins (should sum to 8px).
|
||||||
@@ -323,38 +249,42 @@ function ListBoxInline({ options, layout }) {
|
|||||||
layoutOrder: layoutOptions.layoutOrder,
|
layoutOrder: layoutOptions.layoutOrder,
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchIconComp = constraints?.active ? (
|
|
||||||
<SearchIcon title={translator.get('Listbox.Search')} size="large" style={{ fontSize: '12px', padding: '7px' }} />
|
|
||||||
) : (
|
|
||||||
<IconButton
|
|
||||||
onClick={onShowSearch}
|
|
||||||
tabIndex={-1}
|
|
||||||
title={translator.get('Listbox.Search')}
|
|
||||||
size="large"
|
|
||||||
disableRipple
|
|
||||||
data-testid="search-toggle-btn"
|
|
||||||
>
|
|
||||||
<SearchIcon style={{ fontSize: '12px' }} />
|
|
||||||
</IconButton>
|
|
||||||
);
|
|
||||||
|
|
||||||
const lockIconComp = selectDisabled() ? (
|
|
||||||
<Lock size="large" style={{ fontSize: '12px', padding: '7px' }} />
|
|
||||||
) : (
|
|
||||||
<IconButton title={translator.get('SelectionToolbar.ClickToUnlock')} tabIndex={-1} onClick={unlock} size="large">
|
|
||||||
<Lock disableRipple style={{ fontSize: '12px' }} />
|
|
||||||
</IconButton>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isInvalid) {
|
if (isInvalid) {
|
||||||
renderedCallback?.();
|
renderedCallback?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
const listBoxMinHeight = showToolbarWithTitle ? DENSE_ROW_HEIGHT + SCROLL_BAR_WIDTH : 0;
|
const listBoxMinHeight = showAttachedToolbar ? DENSE_ROW_HEIGHT + SCROLL_BAR_WIDTH : 0;
|
||||||
|
|
||||||
|
const listBoxHeader = (
|
||||||
|
<ListBoxHeader
|
||||||
|
selectDisabled={selectDisabled}
|
||||||
|
showSearchIcon={showSearchIcon}
|
||||||
|
isDrillDown={isDrillDown}
|
||||||
|
onShowSearch={handleShowSearch}
|
||||||
|
isPopover={isPopover}
|
||||||
|
showToolbar={showToolbar}
|
||||||
|
isDirectQuery={isDirectQuery}
|
||||||
|
autoConfirm={autoConfirm}
|
||||||
|
showDetachedToolbarOnly={showDetachedToolbarOnly}
|
||||||
|
layout={layout}
|
||||||
|
translator={translator}
|
||||||
|
styles={styles}
|
||||||
|
isRtl={isRtl}
|
||||||
|
showLock={showLock}
|
||||||
|
constraints={constraints}
|
||||||
|
classes={classes}
|
||||||
|
containerRect={containerRect}
|
||||||
|
containerRef={containerRef}
|
||||||
|
model={model}
|
||||||
|
selectionState={selectionState}
|
||||||
|
selections={selections}
|
||||||
|
keyboard={keyboard}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showDetachedToolbarOnly && <ActionsToolbar direction={direction} {...getActionToolbarProps(true)} />}
|
{showDetachedToolbarOnly && listBoxHeader}
|
||||||
<StyledGrid
|
<StyledGrid
|
||||||
className="listbox-container"
|
className="listbox-container"
|
||||||
container
|
container
|
||||||
@@ -374,47 +304,7 @@ function ListBoxInline({ options, layout }) {
|
|||||||
isGridMode={isGridMode}
|
isGridMode={isGridMode}
|
||||||
aria-label={keyboard.active ? translator.get('Listbox.ScreenReaderInstructions') : ''}
|
aria-label={keyboard.active ? translator.get('Listbox.ScreenReaderInstructions') : ''}
|
||||||
>
|
>
|
||||||
{showToolbarWithTitle && (
|
{showAttachedToolbar && listBoxHeader}
|
||||||
<StyledGridHeader
|
|
||||||
item
|
|
||||||
container
|
|
||||||
styles={styles}
|
|
||||||
isRtl={isRtl}
|
|
||||||
marginY={1}
|
|
||||||
paddingLeft={`${headerPaddingLeft}px`}
|
|
||||||
paddingRight={`${headerPaddingRight}px`}
|
|
||||||
>
|
|
||||||
{showIcons && (
|
|
||||||
<Grid item container alignItems="center" width={iconsWidth}>
|
|
||||||
{isLocked ? lockIconComp : showSearchIcon && searchIconComp}
|
|
||||||
{isDrillDown && (
|
|
||||||
<DrillDownIcon
|
|
||||||
tabIndex={-1}
|
|
||||||
title={translator.get('Listbox.DrillDown')}
|
|
||||||
size="large"
|
|
||||||
style={{ fontSize: '12px' }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
<Grid
|
|
||||||
item
|
|
||||||
xs
|
|
||||||
minWidth={0} // needed to text-overflow see: https://css-tricks.com/flexbox-truncated-text/
|
|
||||||
justifyContent={isRtl ? 'flex-end' : 'flex-start'}
|
|
||||||
className={classes.listBoxHeader}
|
|
||||||
>
|
|
||||||
{showTitle && (
|
|
||||||
<Title variant="h6" noWrap ref={titleRef} title={layout.title} styles={styles}>
|
|
||||||
{layout.title}
|
|
||||||
</Title>
|
|
||||||
)}
|
|
||||||
</Grid>
|
|
||||||
<Grid item display="flex">
|
|
||||||
<ActionsToolbar direction={direction} {...getActionToolbarProps(isToolbarDetached)} />
|
|
||||||
</Grid>
|
|
||||||
</StyledGridHeader>
|
|
||||||
)}
|
|
||||||
<Grid
|
<Grid
|
||||||
item
|
item
|
||||||
container
|
container
|
||||||
@@ -437,7 +327,7 @@ function ListBoxInline({ options, layout }) {
|
|||||||
wildCardSearch={wildCardSearch}
|
wildCardSearch={wildCardSearch}
|
||||||
searchEnabled={searchEnabled}
|
searchEnabled={searchEnabled}
|
||||||
direction={direction}
|
direction={direction}
|
||||||
hide={showSearchIcon && onShowSearch}
|
hide={showSearchIcon && handleShowSearch}
|
||||||
styles={styles}
|
styles={styles}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -140,8 +140,12 @@ export default function ListBoxPopover({
|
|||||||
anchorEl={alignTo.current}
|
anchorEl={alignTo.current}
|
||||||
anchorOrigin={anchorOrigin}
|
anchorOrigin={anchorOrigin}
|
||||||
transformOrigin={transformOrigin}
|
transformOrigin={transformOrigin}
|
||||||
PaperProps={{
|
slotProps={{
|
||||||
style: { minWidth: '250px' },
|
paper: {
|
||||||
|
style: {
|
||||||
|
minWidth: '250px',
|
||||||
|
},
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Grid container direction="column" gap={0} ref={containerRef}>
|
<Grid container direction="column" gap={0} ref={containerRef}>
|
||||||
@@ -160,6 +164,7 @@ export default function ListBoxPopover({
|
|||||||
<Grid item xs />
|
<Grid item xs />
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<ActionsToolbar
|
<ActionsToolbar
|
||||||
|
layout={layout}
|
||||||
more={{
|
more={{
|
||||||
enabled: !isLocked,
|
enabled: !isLocked,
|
||||||
actions: listboxSelectionToolbarItems,
|
actions: listboxSelectionToolbarItems,
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ jest.mock('../interactions/keyboard-navigation/keyboard-nav-container', () => ({
|
|||||||
describe('<ListboxInline />', () => {
|
describe('<ListboxInline />', () => {
|
||||||
const app = { key: 'app' };
|
const app = { key: 'app' };
|
||||||
|
|
||||||
let options;
|
|
||||||
let useState;
|
let useState;
|
||||||
let useEffect;
|
let useEffect;
|
||||||
let useCallback;
|
let useCallback;
|
||||||
@@ -49,6 +48,7 @@ describe('<ListboxInline />', () => {
|
|||||||
let render;
|
let render;
|
||||||
let getListboxInlineKeyboardNavigation;
|
let getListboxInlineKeyboardNavigation;
|
||||||
let InstanceContext;
|
let InstanceContext;
|
||||||
|
let defaultOptions;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
useState = jest.fn();
|
useState = jest.fn();
|
||||||
@@ -111,7 +111,7 @@ describe('<ListboxInline />', () => {
|
|||||||
removeListener: jest.fn(),
|
removeListener: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
options = {
|
defaultOptions = {
|
||||||
app,
|
app,
|
||||||
title: 'title',
|
title: 'title',
|
||||||
direction: 'vertical',
|
direction: 'vertical',
|
||||||
@@ -161,12 +161,13 @@ describe('<ListboxInline />', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const theme = createTheme('dark');
|
const theme = createTheme('dark');
|
||||||
|
|
||||||
render = async () => {
|
render = async (options = {}) => {
|
||||||
|
const mergedOptions = { ...defaultOptions, ...options };
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
renderer = create(
|
renderer = create(
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<InstanceContext.Provider value={{ translator: { get: (s) => s, language: () => 'sv' } }}>
|
<InstanceContext.Provider value={{ translator: { get: (s) => s, language: () => 'sv' } }}>
|
||||||
<ListBoxInline options={options} />
|
<ListBoxInline options={mergedOptions} />
|
||||||
</InstanceContext.Provider>
|
</InstanceContext.Provider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
@@ -201,8 +202,8 @@ describe('<ListboxInline />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should render properly with search toggle option', async () => {
|
test('should render properly with search toggle option', async () => {
|
||||||
options.search = 'toggle';
|
const options = { search: 'toggle' };
|
||||||
await render();
|
await render(options);
|
||||||
|
|
||||||
const searchToggleBtns = renderer.root
|
const searchToggleBtns = renderer.root
|
||||||
.findAllByProps({ 'data-testid': 'search-toggle-btn' })
|
.findAllByProps({ 'data-testid': 'search-toggle-btn' })
|
||||||
@@ -215,8 +216,8 @@ describe('<ListboxInline />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should render without toolbar', async () => {
|
test('should render without toolbar', async () => {
|
||||||
options.toolbar = false;
|
const options = { toolbar: false };
|
||||||
await render();
|
await render(options);
|
||||||
const actionToolbars = renderer.root.findAllByType(ActionsToolbar);
|
const actionToolbars = renderer.root.findAllByType(ActionsToolbar);
|
||||||
expect(actionToolbars).toHaveLength(0);
|
expect(actionToolbars).toHaveLength(0);
|
||||||
|
|
||||||
@@ -228,8 +229,8 @@ describe('<ListboxInline />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should render without toolbar', async () => {
|
test('should render without toolbar', async () => {
|
||||||
options.search = 'toggle';
|
const options = { search: 'toggle' };
|
||||||
await render();
|
await render(options);
|
||||||
|
|
||||||
expect(ListBoxSearch.mock.calls[0][0]).toMatchObject({
|
expect(ListBoxSearch.mock.calls[0][0]).toMatchObject({
|
||||||
visible: false,
|
visible: false,
|
||||||
@@ -241,8 +242,8 @@ describe('<ListboxInline />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should render without search and show search button', async () => {
|
test('should render without search and show search button', async () => {
|
||||||
options.search = false;
|
const options = { search: false };
|
||||||
await render();
|
await render(options);
|
||||||
const actionToolbars = renderer.root.findAllByType(ActionsToolbar);
|
const actionToolbars = renderer.root.findAllByType(ActionsToolbar);
|
||||||
expect(actionToolbars).toHaveLength(1);
|
expect(actionToolbars).toHaveLength(1);
|
||||||
|
|
||||||
@@ -255,8 +256,8 @@ describe('<ListboxInline />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should render with NOT autoFocus when search is true', async () => {
|
test('should render with NOT autoFocus when search is true', async () => {
|
||||||
options.search = true;
|
const options = { search: true };
|
||||||
await render();
|
await render(options);
|
||||||
|
|
||||||
expect(ListBoxSearch.mock.calls[0][0]).toMatchObject({
|
expect(ListBoxSearch.mock.calls[0][0]).toMatchObject({
|
||||||
visible: true,
|
visible: true,
|
||||||
@@ -265,9 +266,8 @@ describe('<ListboxInline />', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should show toolbar when opened in a popover', async () => {
|
test('should show toolbar when opened in a popover', async () => {
|
||||||
options.search = false;
|
const options = { search: false, isPopover: true };
|
||||||
options.isPopover = true;
|
await render(options);
|
||||||
await render();
|
|
||||||
const actionToolbars = renderer.root.findAllByType(ActionsToolbar);
|
const actionToolbars = renderer.root.findAllByType(ActionsToolbar);
|
||||||
expect(actionToolbars).toHaveLength(1);
|
expect(actionToolbars).toHaveLength(1);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { Grid, IconButton } from '@mui/material';
|
||||||
|
import Lock from '@nebula.js/ui/icons/lock';
|
||||||
|
import { unlock } from '@nebula.js/ui/icons/unlock';
|
||||||
|
import SearchIcon from '@nebula.js/ui/icons/search';
|
||||||
|
import DrillDownIcon from '@nebula.js/ui/icons/drill-down';
|
||||||
|
import ActionsToolbar from '../../../ActionsToolbar';
|
||||||
|
import showToolbarDetached from '../../interactions/listbox-show-toolbar-detached';
|
||||||
|
import getListboxActionProps from '../../interactions/listbox-action-props';
|
||||||
|
import createListboxSelectionToolbar from '../../interactions/listbox-selection-toolbar';
|
||||||
|
import { BUTTON_ICON_WIDTH, CELL_PADDING_LEFT, HEADER_PADDING_RIGHT, ICON_PADDING } from '../../constants';
|
||||||
|
import hasSelections from '../../assets/has-selections';
|
||||||
|
import { HeaderTitle, StyledGridHeader, UnlockCoverButton, iconStyle } from './ListBoxHeaderComponents';
|
||||||
|
|
||||||
|
// ms that needs to pass before the lock button can be toggled again
|
||||||
|
const lockTimeFrameMs = 500;
|
||||||
|
let lastTime = 0;
|
||||||
|
|
||||||
|
function getToggleLock({ isLocked, setLocked, settingLockedState, model, setSettingLockedState }) {
|
||||||
|
return () => {
|
||||||
|
const now = new Date();
|
||||||
|
const curTime = now - lastTime;
|
||||||
|
if (curTime < lockTimeFrameMs) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
if (settingLockedState) {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
setSettingLockedState(true);
|
||||||
|
lastTime = now;
|
||||||
|
const func = isLocked ? model.unlock : model.lock;
|
||||||
|
setLocked(!isLocked);
|
||||||
|
return func
|
||||||
|
.call(model, '/qListObjectDef')
|
||||||
|
.catch(() => {
|
||||||
|
setLocked(isLocked); // revert to the layout value
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setSettingLockedState(false);
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ListBoxHeader({
|
||||||
|
layout,
|
||||||
|
translator,
|
||||||
|
styles,
|
||||||
|
isRtl,
|
||||||
|
showLock,
|
||||||
|
selectDisabled,
|
||||||
|
showSearchIcon,
|
||||||
|
isDrillDown,
|
||||||
|
constraints,
|
||||||
|
onShowSearch,
|
||||||
|
classes,
|
||||||
|
containerRect,
|
||||||
|
isPopover,
|
||||||
|
showToolbar,
|
||||||
|
showDetachedToolbarOnly,
|
||||||
|
containerRef,
|
||||||
|
model,
|
||||||
|
selectionState,
|
||||||
|
isDirectQuery,
|
||||||
|
selections,
|
||||||
|
keyboard,
|
||||||
|
autoConfirm,
|
||||||
|
}) {
|
||||||
|
const [isToolbarDetached, setIsToolbarDetached] = useState(showDetachedToolbarOnly);
|
||||||
|
const [isLocked, setLocked] = useState(layout?.qListObject?.qDimensionInfo?.qLocked);
|
||||||
|
const [settingLockedState, setSettingLockedState] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLocked(layout?.qListObject?.qDimensionInfo?.qLocked);
|
||||||
|
}, [layout?.qListObject?.qDimensionInfo?.qLocked]);
|
||||||
|
|
||||||
|
const titleRef = useRef(null);
|
||||||
|
|
||||||
|
const showUnlock = showLock && isLocked;
|
||||||
|
const showLockIcon = !showLock && isLocked; // shows instead of the cover button when field/dim is locked.
|
||||||
|
const showLeftIcon = showSearchIcon || showLockIcon || isDrillDown; // the left-most icon outside of the actions/selections toolbar.
|
||||||
|
|
||||||
|
const paddingLeft = CELL_PADDING_LEFT - (showLeftIcon ? ICON_PADDING : 0);
|
||||||
|
const paddingRight = isRtl ? CELL_PADDING_LEFT - (showLeftIcon ? ICON_PADDING : 0) : HEADER_PADDING_RIGHT;
|
||||||
|
|
||||||
|
// Calculate explicit width for icons container. Search and lock cannot exist combined.
|
||||||
|
const iconsWidth =
|
||||||
|
(showSearchIcon ? BUTTON_ICON_WIDTH : 0) +
|
||||||
|
(showLockIcon ? BUTTON_ICON_WIDTH : 0) +
|
||||||
|
(isDrillDown ? BUTTON_ICON_WIDTH : 0);
|
||||||
|
|
||||||
|
const toggleLock = getToggleLock({ isLocked, setLocked, settingLockedState, setSettingLockedState, model });
|
||||||
|
|
||||||
|
const listboxSelectionToolbarItems = createListboxSelectionToolbar({
|
||||||
|
layout,
|
||||||
|
model,
|
||||||
|
translator,
|
||||||
|
selectionState,
|
||||||
|
isDirectQuery,
|
||||||
|
});
|
||||||
|
|
||||||
|
const extraItems =
|
||||||
|
isLocked || !showLock
|
||||||
|
? undefined
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
key: 'lock',
|
||||||
|
type: undefined,
|
||||||
|
label: translator.get('SelectionToolbar.ClickToLock'),
|
||||||
|
getSvgIconShape: unlock,
|
||||||
|
enabled: () => !settingLockedState && !selectDisabled() && hasSelections(layout),
|
||||||
|
action: toggleLock,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const searchIconComp = constraints?.active ? (
|
||||||
|
<SearchIcon
|
||||||
|
title={translator.get('Listbox.Search')}
|
||||||
|
size="large"
|
||||||
|
style={{ ...iconStyle, padding: `${ICON_PADDING}px` }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<IconButton
|
||||||
|
onClick={onShowSearch}
|
||||||
|
tabIndex={-1}
|
||||||
|
title={translator.get('Listbox.Search')}
|
||||||
|
size="large"
|
||||||
|
disableRipple
|
||||||
|
data-testid="search-toggle-btn"
|
||||||
|
>
|
||||||
|
<SearchIcon style={iconStyle} />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!titleRef.current || !containerRect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mustShowDetached = showToolbarDetached({
|
||||||
|
containerRect,
|
||||||
|
titleRef,
|
||||||
|
iconsWidth,
|
||||||
|
paddingLeft,
|
||||||
|
paddingRight,
|
||||||
|
});
|
||||||
|
const isDetached = showDetachedToolbarOnly || mustShowDetached;
|
||||||
|
setIsToolbarDetached(isDetached);
|
||||||
|
}, [
|
||||||
|
iconsWidth,
|
||||||
|
paddingLeft,
|
||||||
|
paddingRight,
|
||||||
|
titleRef.current,
|
||||||
|
showDetachedToolbarOnly,
|
||||||
|
Object.entries(containerRect || {})
|
||||||
|
.sort()
|
||||||
|
.join(','),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const toolbarProps = getListboxActionProps({
|
||||||
|
isDetached: isPopover ? false : isToolbarDetached,
|
||||||
|
showToolbar,
|
||||||
|
containerRef,
|
||||||
|
isLocked,
|
||||||
|
extraItems,
|
||||||
|
listboxSelectionToolbarItems,
|
||||||
|
selections,
|
||||||
|
keyboard,
|
||||||
|
autoConfirm,
|
||||||
|
});
|
||||||
|
|
||||||
|
const actionsToolbar = <ActionsToolbar isRtl={isRtl} layout={layout} {...toolbarProps} />;
|
||||||
|
|
||||||
|
if (showDetachedToolbarOnly) {
|
||||||
|
return actionsToolbar;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always show a lock symbol when locked and showLock is false
|
||||||
|
const lockedIconComp = showLockIcon ? (
|
||||||
|
<Lock size="large" style={{ ...iconStyle, padding: `${ICON_PADDING}px` }} />
|
||||||
|
) : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledGridHeader
|
||||||
|
item
|
||||||
|
container
|
||||||
|
styles={styles}
|
||||||
|
isRtl={isRtl}
|
||||||
|
marginY={1}
|
||||||
|
paddingLeft={`${paddingLeft}px`}
|
||||||
|
paddingRight={`${paddingRight}px`}
|
||||||
|
className="header-container"
|
||||||
|
>
|
||||||
|
{showUnlock && (
|
||||||
|
<UnlockCoverButton
|
||||||
|
isLoading={settingLockedState}
|
||||||
|
translator={translator}
|
||||||
|
toggleLock={toggleLock}
|
||||||
|
keyboard={keyboard}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showLeftIcon && (
|
||||||
|
<Grid item container alignItems="center" width={iconsWidth} className="header-action-container">
|
||||||
|
{lockedIconComp || (showSearchIcon && searchIconComp)}
|
||||||
|
{isDrillDown && (
|
||||||
|
<DrillDownIcon
|
||||||
|
tabIndex={-1}
|
||||||
|
title={translator.get('Listbox.DrillDown')}
|
||||||
|
style={{ ...iconStyle, padding: `${ICON_PADDING}px` }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
<Grid
|
||||||
|
item
|
||||||
|
xs
|
||||||
|
minWidth={0} // needed to text-overflow see: https://css-tricks.com/flexbox-truncated-text/
|
||||||
|
justifyContent={isRtl ? 'flex-end' : 'flex-start'}
|
||||||
|
className={classes.listBoxHeader}
|
||||||
|
>
|
||||||
|
<HeaderTitle variant="h6" noWrap ref={titleRef} title={layout.title} styles={styles}>
|
||||||
|
{layout.title}
|
||||||
|
</HeaderTitle>
|
||||||
|
</Grid>
|
||||||
|
<Grid item display="flex">
|
||||||
|
{actionsToolbar}
|
||||||
|
</Grid>
|
||||||
|
</StyledGridHeader>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import { ButtonBase, CircularProgress, Grid, Typography } from '@mui/material';
|
||||||
|
import Lock from '@nebula.js/ui/icons/lock';
|
||||||
|
import { ICON_PADDING } from '../../constants';
|
||||||
|
|
||||||
|
export const iconStyle = {
|
||||||
|
fontSize: '12px',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UnlockButton = styled(ButtonBase)(({ theme, isLoading }) => ({
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
height: 48,
|
||||||
|
zIndex: 2, // so it goes on top of action buttons
|
||||||
|
background: theme.palette.custom.disabledBackground,
|
||||||
|
opacity: 1,
|
||||||
|
color: theme.palette.custom.disabledContrastText,
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: isLoading ? 'center' : 'flex-start',
|
||||||
|
paddingLeft: 16,
|
||||||
|
paddingRight: 16,
|
||||||
|
borderRadius: 0,
|
||||||
|
'& *, & p': {
|
||||||
|
color: theme.palette.custom.disabledContrastText,
|
||||||
|
},
|
||||||
|
'& i': {
|
||||||
|
padding: `${ICON_PADDING}px`,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledGridHeader = styled(Grid, { shouldForwardProp: (p) => !['styles', 'isRtl'].includes(p) })(
|
||||||
|
({ styles, isRtl }) => ({
|
||||||
|
flexDirection: isRtl ? 'row-reverse' : 'row',
|
||||||
|
wrap: 'nowrap',
|
||||||
|
minHeight: 32,
|
||||||
|
alignContent: 'center',
|
||||||
|
...styles.header,
|
||||||
|
'& *': {
|
||||||
|
color: styles.header.color,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export const HeaderTitle = styled(Typography)(({ styles }) => ({
|
||||||
|
...styles.header,
|
||||||
|
display: 'block', // needed for text-overflow to work
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingRight: '1px', // make place for italic font style
|
||||||
|
}));
|
||||||
|
|
||||||
|
export function UnlockCoverButton({ translator, toggleLock, keyboard, isLoading }) {
|
||||||
|
const fontSize = '14px';
|
||||||
|
const unLockText = translator.get('SelectionToolbar.ClickToUnlock');
|
||||||
|
const component = (
|
||||||
|
<UnlockButton
|
||||||
|
title={unLockText}
|
||||||
|
tabIndex={keyboard.enabled ? 0 : -1}
|
||||||
|
onClick={toggleLock}
|
||||||
|
data-testid="listbox-unlock-button"
|
||||||
|
id="listbox-unlock-button"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<CircularProgress size={16} variant="indeterminate" color="primary" />
|
||||||
|
) : (
|
||||||
|
<Lock disableRipple style={iconStyle} />
|
||||||
|
)}
|
||||||
|
{!isLoading && <Typography fontSize={fontSize}>{unLockText}</Typography>}
|
||||||
|
</UnlockButton>
|
||||||
|
);
|
||||||
|
return component;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import ListBoxHeader from './ListBoxHeader';
|
||||||
|
|
||||||
|
export default ListBoxHeader;
|
||||||
@@ -172,13 +172,14 @@ function RowColumn({ index, rowIndex, columnIndex, style, data }) {
|
|||||||
paddingLeft: isRtl ? 8 : checkboxes ? 0 : undefined,
|
paddingLeft: isRtl ? 8 : checkboxes ? 0 : undefined,
|
||||||
paddingRight: checkboxes ? 0 : isRtl ? 8 : 0,
|
paddingRight: checkboxes ? 0 : isRtl ? 8 : 0,
|
||||||
justifyContent: valueTextAlign,
|
justifyContent: valueTextAlign,
|
||||||
|
textAlign: valueTextAlign,
|
||||||
};
|
};
|
||||||
|
|
||||||
const isFirstElement = index === 0;
|
const isFirstElement = index === 0;
|
||||||
|
|
||||||
const showLock = isSelected && isLocked;
|
const showLockIcon = isSelected && isLocked;
|
||||||
const showTick = !checkboxes && isSelected && !isLocked;
|
const showTickIcon = !checkboxes && isSelected && !isLocked;
|
||||||
const showIcon = !checkboxes && sizePermitsTickOrLock;
|
const showAnyIcon = !checkboxes && sizePermitsTickOrLock;
|
||||||
const cellPaddingRight = checkboxes || !sizePermitsTickOrLock;
|
const cellPaddingRight = checkboxes || !sizePermitsTickOrLock;
|
||||||
|
|
||||||
const ariaLabel = getValueLabel({
|
const ariaLabel = getValueLabel({
|
||||||
@@ -288,10 +289,10 @@ function RowColumn({ index, rowIndex, columnIndex, style, data }) {
|
|||||||
|
|
||||||
{freqIsAllowed && <Frequency cell={cell} checkboxes={checkboxes} dense={dense} showGray={showGray} />}
|
{freqIsAllowed && <Frequency cell={cell} checkboxes={checkboxes} dense={dense} showGray={showGray} />}
|
||||||
|
|
||||||
{showIcon && (
|
{showAnyIcon && (
|
||||||
<Grid item className={classes.icon}>
|
<Grid item className={classes.icon}>
|
||||||
{showLock && <Lock style={iconStyles} size="small" />}
|
{showLockIcon && <Lock style={iconStyles} size="small" />}
|
||||||
{showTick && <Tick style={iconStyles} size="small" />}
|
{showTickIcon && <Tick style={iconStyles} size="small" />}
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
</ItemGrid>
|
</ItemGrid>
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export default function ListBoxSearch({
|
|||||||
|
|
||||||
function focusRow(container) {
|
function focusRow(container) {
|
||||||
const row = container?.querySelector('.last-focused') || container?.querySelector('[role="row"]:first-child');
|
const row = container?.querySelector('.last-focused') || container?.querySelector('[role="row"]:first-child');
|
||||||
row.setAttribute('tabIndex', 0);
|
row?.setAttribute('tabIndex', 0);
|
||||||
row?.focus();
|
row?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
|
/* eslint-disable react/jsx-no-constructed-context-values */
|
||||||
|
/* eslint-disable no-import-assign */
|
||||||
|
import React from 'react';
|
||||||
|
import renderer, { act } from 'react-test-renderer';
|
||||||
|
import DrillDownIcon from '@nebula.js/ui/icons/drill-down';
|
||||||
|
import { createTheme, ThemeProvider } from '@nebula.js/ui/theme';
|
||||||
|
import Lock from '@nebula.js/ui/icons/lock';
|
||||||
|
import { unlock } from '@nebula.js/ui/icons/unlock';
|
||||||
|
import ListBoxHeader from '../ListBoxHeader';
|
||||||
|
import * as InstanceContextModule from '../../../../contexts/InstanceContext';
|
||||||
|
import * as ActionsToolbarModule from '../../../ActionsToolbar';
|
||||||
|
import * as HeaderComponents from '../ListBoxHeader/ListBoxHeaderComponents';
|
||||||
|
import * as hasSelectionsModule from '../../assets/has-selections';
|
||||||
|
|
||||||
|
const { StyledGridHeader, UnlockButton } = HeaderComponents;
|
||||||
|
|
||||||
|
const selections = {
|
||||||
|
canClear: () => true,
|
||||||
|
canConfirm: () => true,
|
||||||
|
canCancel: () => true,
|
||||||
|
};
|
||||||
|
const styles = { content: {}, header: { color: 'red' }, selections: {}, search: {}, background: {} };
|
||||||
|
let rendererInst;
|
||||||
|
|
||||||
|
const translator = { get: jest.fn().mockImplementation((v) => v) };
|
||||||
|
const theme = createTheme('dark');
|
||||||
|
|
||||||
|
const model = {
|
||||||
|
lock: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||||
|
unlock: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||||
|
selectListObjectAll: jest.fn(),
|
||||||
|
selectListObjectPossible: jest.fn(),
|
||||||
|
selectListObjectAlternative: jest.fn(),
|
||||||
|
selectListObjectExcluded: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
function getDefaultProps() {
|
||||||
|
const containerRef = React.createRef();
|
||||||
|
const defaultProps = {
|
||||||
|
layout: { title: 'The title', qListObject: { qDimensionInfo: { qLocked: false } } },
|
||||||
|
translator,
|
||||||
|
styles,
|
||||||
|
isRtl: false,
|
||||||
|
showLock: true,
|
||||||
|
selectDisabled: () => false,
|
||||||
|
showSearchIcon: 'toggle',
|
||||||
|
isDrillDown: false,
|
||||||
|
constraints: { active: false },
|
||||||
|
onShowSearch: () => {},
|
||||||
|
classes: { listBoxHeader: 'listBoxHeader' },
|
||||||
|
containerRect: { width: 200 },
|
||||||
|
isPopover: false,
|
||||||
|
showToolbar: true,
|
||||||
|
showDetachedToolbarOnly: false,
|
||||||
|
containerRef,
|
||||||
|
model,
|
||||||
|
selectionState: {
|
||||||
|
clearItemStates: jest.fn(),
|
||||||
|
},
|
||||||
|
isDirectQuery: false,
|
||||||
|
selections,
|
||||||
|
keyboard: { enabled: true },
|
||||||
|
autoConfirm: false,
|
||||||
|
};
|
||||||
|
return defaultProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InstanceContext = React.createContext();
|
||||||
|
|
||||||
|
let component;
|
||||||
|
|
||||||
|
const render = async (overrideProps = {}) => {
|
||||||
|
const defaultProps = getDefaultProps();
|
||||||
|
const mergedProps = { ...defaultProps, ...overrideProps };
|
||||||
|
component = (
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<InstanceContext.Provider value={{ translator }}>
|
||||||
|
<ListBoxHeader {...mergedProps} />
|
||||||
|
</InstanceContext.Provider>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
await act(() => {
|
||||||
|
rendererInst = renderer.create(component);
|
||||||
|
});
|
||||||
|
return rendererInst;
|
||||||
|
};
|
||||||
|
|
||||||
|
let ActionsToolbar;
|
||||||
|
|
||||||
|
// Mock the useRef module
|
||||||
|
jest.mock('react', () => ({
|
||||||
|
...jest.requireActual('react'), // Use the actual implementation of React
|
||||||
|
useRef: jest.fn(),
|
||||||
|
useCallback: (func) => func,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let HeaderTitle;
|
||||||
|
let hasSelections;
|
||||||
|
|
||||||
|
function HeaderTitleMock() {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('<ListBoxHeader />', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
hasSelections = jest.spyOn(hasSelectionsModule, 'default').mockReturnValue(true);
|
||||||
|
HeaderComponents.HeaderTitle = HeaderTitleMock;
|
||||||
|
HeaderTitle = HeaderComponents.HeaderTitle;
|
||||||
|
InstanceContextModule.default = InstanceContext;
|
||||||
|
const ActionsToolbarElement = <div id="test-actions-toolbar" />;
|
||||||
|
ActionsToolbar = jest.spyOn(ActionsToolbarModule, 'default').mockImplementation(() => ActionsToolbarElement);
|
||||||
|
jest.spyOn(React, 'useRef').mockReturnValue({ current: { clientWidth: 100, scrollWidth: 100, offsetWidth: 100 } });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
rendererInst.unmount();
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render the header with title and attached actions toolbar', async () => {
|
||||||
|
const testRenderer = await render();
|
||||||
|
const testInstance = testRenderer.root;
|
||||||
|
|
||||||
|
// Find by type.
|
||||||
|
const titles = testInstance.findAllByType(HeaderTitle);
|
||||||
|
const drillDowns = testInstance.findAllByType(DrillDownIcon);
|
||||||
|
const actionsToolbars = testInstance.findAllByType(ActionsToolbar);
|
||||||
|
const locks = testInstance.findAllByType(Lock);
|
||||||
|
const unlocks = testInstance.findAllByType(unlock);
|
||||||
|
|
||||||
|
// Check existence.
|
||||||
|
expect(titles).toHaveLength(1);
|
||||||
|
expect(drillDowns).toHaveLength(0);
|
||||||
|
expect(actionsToolbars).toHaveLength(1);
|
||||||
|
expect(locks).toHaveLength(0);
|
||||||
|
expect(unlocks).toHaveLength(0);
|
||||||
|
|
||||||
|
// Dig in to Title.
|
||||||
|
const titleProps = titles[0].props;
|
||||||
|
expect(titleProps.title).toEqual('The title');
|
||||||
|
expect(titleProps.children).toEqual('The title');
|
||||||
|
expect(titleProps.styles.header.color).toEqual('red');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render a detached toolbar when told to do so', async () => {
|
||||||
|
const testRenderer = await render({ showDetachedToolbarOnly: true });
|
||||||
|
const testInstance = testRenderer.root;
|
||||||
|
|
||||||
|
const titles = testInstance.findAllByType(HeaderTitle);
|
||||||
|
const actionsToolbars = testInstance.findAllByType(ActionsToolbar);
|
||||||
|
const styledGridHeaders = testInstance.findAllByType(StyledGridHeader);
|
||||||
|
|
||||||
|
// Check existence.
|
||||||
|
expect(titles).toHaveLength(0);
|
||||||
|
expect(actionsToolbars).toHaveLength(1);
|
||||||
|
expect(styledGridHeaders).toHaveLength(0);
|
||||||
|
|
||||||
|
// Dig in to const actionsToolbar props
|
||||||
|
expect(actionsToolbars[0].props.isDetached).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render a detached toolbar when space is limited', async () => {
|
||||||
|
const containerRect = { width: 20 };
|
||||||
|
const testRenderer = await render({ showDetachedToolbarOnly: false, containerRect });
|
||||||
|
const testInstance = testRenderer.root;
|
||||||
|
|
||||||
|
const titles = testInstance.findAllByType(HeaderTitle);
|
||||||
|
const actionsToolbars = testInstance.findAllByType(ActionsToolbar);
|
||||||
|
const styledGridHeaders = testInstance.findAllByType(StyledGridHeader);
|
||||||
|
|
||||||
|
// Check existence.
|
||||||
|
expect(titles).toHaveLength(1);
|
||||||
|
expect(actionsToolbars).toHaveLength(1);
|
||||||
|
expect(styledGridHeaders).toHaveLength(1);
|
||||||
|
|
||||||
|
// Verify it is detached.
|
||||||
|
expect(actionsToolbars[0].props.isDetached).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('trigger toggle search field when pressing search icon in toggle mode', async () => {
|
||||||
|
const onShowSearch = jest.fn();
|
||||||
|
const testRenderer = await render({ search: 'toggle', onShowSearch });
|
||||||
|
const testInstance = testRenderer.root;
|
||||||
|
|
||||||
|
const searchButton = testInstance.findByProps({ 'data-testid': 'search-toggle-btn' });
|
||||||
|
|
||||||
|
// Check existence.
|
||||||
|
expect(searchButton).toBeTruthy();
|
||||||
|
expect(onShowSearch).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
// Trigger toggle search and verify it was called.
|
||||||
|
await act(() => {
|
||||||
|
searchButton.props.onClick();
|
||||||
|
});
|
||||||
|
expect(onShowSearch).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('There should be a lock button inside the actions toolbar', async () => {
|
||||||
|
hasSelections.mockReturnValue(true);
|
||||||
|
const testRenderer = await render({ showSearchIcon: false, showLock: true, isPopover: true });
|
||||||
|
const testInstance = testRenderer.root;
|
||||||
|
|
||||||
|
const [actionsToolbar] = testInstance.findAllByType(ActionsToolbar);
|
||||||
|
const unlockCoverButtons = testInstance.findAllByType(UnlockButton);
|
||||||
|
|
||||||
|
// Check existence.
|
||||||
|
const btns = actionsToolbar.props.extraItems;
|
||||||
|
expect(btns).toHaveLength(1);
|
||||||
|
expect(btns[0].key).toEqual('lock');
|
||||||
|
expect(btns[0].enabled()).toEqual(true);
|
||||||
|
|
||||||
|
// Ensure unlock is not visible
|
||||||
|
expect(unlockCoverButtons).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('There should be an unlock cover button on top of the actions toolbar', async () => {
|
||||||
|
const layout = { title: 'The title', qListObject: { qDimensionInfo: { qLocked: true } } };
|
||||||
|
hasSelections.mockReturnValue(false);
|
||||||
|
const testRenderer = await render({
|
||||||
|
layout,
|
||||||
|
showSearchIcon: false,
|
||||||
|
showLock: true,
|
||||||
|
isPopover: true,
|
||||||
|
});
|
||||||
|
const testInstance = testRenderer.root;
|
||||||
|
const unlockCoverButtons = testInstance.findAllByType(UnlockButton);
|
||||||
|
|
||||||
|
// Ensure unlock is visible
|
||||||
|
expect(unlockCoverButtons).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -52,14 +52,17 @@ export default function useTempKeyboard({ containerRef, enabled }) {
|
|||||||
const lastSelectedRow = c?.querySelector('.value.last-focused');
|
const lastSelectedRow = c?.querySelector('.value.last-focused');
|
||||||
const firstRowElement = c?.querySelector('.value.selector, .value');
|
const firstRowElement = c?.querySelector('.value.selector, .value');
|
||||||
const confirmButton = c?.querySelector('.actions-toolbar-default-actions .actions-toolbar-confirm');
|
const confirmButton = c?.querySelector('.actions-toolbar-default-actions .actions-toolbar-confirm');
|
||||||
const elementToFocus = searchField || lastSelectedRow || firstRowElement || confirmButton;
|
const unlockCoverButton = c?.querySelector('#listbox-unlock-button');
|
||||||
|
const elementToFocus = searchField || lastSelectedRow || firstRowElement || unlockCoverButton || confirmButton;
|
||||||
elementToFocus?.setAttribute('tabIndex', 0);
|
elementToFocus?.setAttribute('tabIndex', 0);
|
||||||
elementToFocus?.focus();
|
elementToFocus?.focus();
|
||||||
},
|
},
|
||||||
focusSelection() {
|
focusSelection() {
|
||||||
|
const unlockCoverButton = document.querySelector('#listbox-unlock-button');
|
||||||
const confirmButton = document.querySelector('.actions-toolbar-default-actions .actions-toolbar-confirm');
|
const confirmButton = document.querySelector('.actions-toolbar-default-actions .actions-toolbar-confirm');
|
||||||
confirmButton?.setAttribute('tabIndex', 0);
|
const btnToFocus = unlockCoverButton || confirmButton;
|
||||||
confirmButton?.focus();
|
btnToFocus?.setAttribute('tabIndex', 0);
|
||||||
|
btnToFocus?.focus();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
import showToolbarDetached from '../listbox-show-toolbar-detached';
|
import showToolbarDetached from '../listbox-show-toolbar-detached';
|
||||||
|
|
||||||
const iconsWidth = 28;
|
const iconsWidth = 28;
|
||||||
const headerPaddingLeft = 9;
|
const paddingLeft = 9;
|
||||||
const headerPaddingRight = 4;
|
const paddingRight = 4;
|
||||||
|
|
||||||
describe('show listbox toolbar detached', () => {
|
describe('show listbox toolbar detached', () => {
|
||||||
it('should return true if there is not enough space for toolbar', () => {
|
it('should return true if there is not enough space for toolbar', () => {
|
||||||
const containerRect = { width: 100 };
|
const containerRect = { width: 100 };
|
||||||
const titleRef = { current: { clientWidth: 50, scrollWidth: 80, offsetWidth: 81 } };
|
const titleRef = { current: { clientWidth: 50, scrollWidth: 80, offsetWidth: 81 } };
|
||||||
expect(
|
expect(showToolbarDetached({ containerRect, titleRef, iconsWidth, paddingLeft, paddingRight })).toStrictEqual(true);
|
||||||
showToolbarDetached({ containerRect, titleRef, iconsWidth, headerPaddingLeft, headerPaddingRight })
|
|
||||||
).toStrictEqual(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true if title is truncated', () => {
|
it('should return true if title is truncated', () => {
|
||||||
const containerRect = { width: 300 };
|
const containerRect = { width: 300 };
|
||||||
const titleRef = {
|
const titleRef = {
|
||||||
current: { clientWidth: 60, scrollWidth: 200, offsetWidth: 199, headerPaddingLeft, headerPaddingRight },
|
current: { clientWidth: 60, scrollWidth: 200, offsetWidth: 199, paddingLeft, paddingRight },
|
||||||
};
|
};
|
||||||
expect(showToolbarDetached({ containerRect, titleRef, iconsWidth })).toStrictEqual(true);
|
expect(showToolbarDetached({ containerRect, titleRef, iconsWidth })).toStrictEqual(true);
|
||||||
});
|
});
|
||||||
@@ -24,7 +22,7 @@ describe('show listbox toolbar detached', () => {
|
|||||||
it('should return false if there is enough space for title and toolbar', () => {
|
it('should return false if there is enough space for title and toolbar', () => {
|
||||||
const containerRect = { width: 300 };
|
const containerRect = { width: 300 };
|
||||||
const titleRef = {
|
const titleRef = {
|
||||||
current: { clientWidth: 60, scrollWidth: 200, offsetWidth: 201, headerPaddingLeft, headerPaddingRight },
|
current: { clientWidth: 60, scrollWidth: 200, offsetWidth: 201, paddingLeft, paddingRight },
|
||||||
};
|
};
|
||||||
expect(showToolbarDetached({ containerRect, titleRef, iconsWidth })).toStrictEqual(false);
|
expect(showToolbarDetached({ containerRect, titleRef, iconsWidth })).toStrictEqual(false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,15 +4,18 @@ export default function getListboxActionProps({
|
|||||||
containerRef,
|
containerRef,
|
||||||
isLocked,
|
isLocked,
|
||||||
listboxSelectionToolbarItems,
|
listboxSelectionToolbarItems,
|
||||||
|
extraItems,
|
||||||
selections,
|
selections,
|
||||||
keyboard,
|
keyboard,
|
||||||
}) {
|
}) {
|
||||||
return {
|
return {
|
||||||
|
isDetached,
|
||||||
show: showToolbar && !isDetached,
|
show: showToolbar && !isDetached,
|
||||||
popover: {
|
popover: {
|
||||||
show: showToolbar && isDetached,
|
show: showToolbar && isDetached,
|
||||||
anchorEl: containerRef.current,
|
anchorEl: containerRef.current,
|
||||||
},
|
},
|
||||||
|
extraItems,
|
||||||
more: {
|
more: {
|
||||||
enabled: !isLocked,
|
enabled: !isLocked,
|
||||||
actions: listboxSelectionToolbarItems,
|
actions: listboxSelectionToolbarItems,
|
||||||
@@ -1,18 +1,11 @@
|
|||||||
export default function showToolbarDetached({
|
export default function showToolbarDetached({ containerRect, titleRef, iconsWidth, paddingLeft, paddingRight }) {
|
||||||
containerRect,
|
|
||||||
titleRef,
|
|
||||||
iconsWidth,
|
|
||||||
headerPaddingLeft,
|
|
||||||
headerPaddingRight,
|
|
||||||
}) {
|
|
||||||
const containerWidth = containerRect.width;
|
const containerWidth = containerRect.width;
|
||||||
const preventTruncation = 2;
|
const preventTruncation = 2;
|
||||||
const padding = headerPaddingLeft + headerPaddingRight;
|
const padding = paddingLeft + paddingRight;
|
||||||
const contentWidth = (titleRef?.current?.clientWidth ?? 0) + iconsWidth + padding + preventTruncation;
|
const contentWidth = (titleRef?.current?.clientWidth ?? 0) + iconsWidth + padding + preventTruncation;
|
||||||
const actionToolbarWidth = 128;
|
const actionToolbarWidth = 128;
|
||||||
const notSufficientSpace = containerWidth < contentWidth + actionToolbarWidth;
|
const notSufficientSpace = containerWidth < contentWidth + actionToolbarWidth;
|
||||||
const isTruncated = titleRef?.current?.scrollWidth > titleRef?.current?.offsetWidth;
|
const isTruncated = titleRef?.current?.scrollWidth > titleRef?.current?.offsetWidth;
|
||||||
const isDetached = !!(notSufficientSpace || isTruncated);
|
const isDetached = !!(notSufficientSpace || isTruncated);
|
||||||
|
|
||||||
return isDetached;
|
return isDetached;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import InstanceContext from '../contexts/InstanceContext';
|
|||||||
|
|
||||||
export default function useDefaultSelectionActions({
|
export default function useDefaultSelectionActions({
|
||||||
api,
|
api,
|
||||||
|
layout,
|
||||||
onConfirm = () => {},
|
onConfirm = () => {},
|
||||||
onCancel = () => {},
|
onCancel = () => {},
|
||||||
onKeyDeactivate = () => {},
|
onKeyDeactivate = () => {},
|
||||||
@@ -17,7 +18,7 @@ export default function useDefaultSelectionActions({
|
|||||||
key: 'clear',
|
key: 'clear',
|
||||||
type: 'icon-button',
|
type: 'icon-button',
|
||||||
label: translator.get('Selection.Clear'),
|
label: translator.get('Selection.Clear'),
|
||||||
enabled: () => api.canClear(),
|
enabled: () => api.canClear(layout),
|
||||||
action: () => api.clear(),
|
action: () => api.clear(),
|
||||||
getSvgIconShape: clearSelections,
|
getSvgIconShape: clearSelections,
|
||||||
},
|
},
|
||||||
@@ -25,7 +26,7 @@ export default function useDefaultSelectionActions({
|
|||||||
key: 'cancel',
|
key: 'cancel',
|
||||||
type: 'icon-button',
|
type: 'icon-button',
|
||||||
label: translator.get('Selection.Cancel'),
|
label: translator.get('Selection.Cancel'),
|
||||||
enabled: () => api.canCancel(),
|
enabled: () => api.canCancel(layout),
|
||||||
action: () => {
|
action: () => {
|
||||||
onCancel();
|
onCancel();
|
||||||
api.cancel();
|
api.cancel();
|
||||||
@@ -41,7 +42,7 @@ export default function useDefaultSelectionActions({
|
|||||||
key: 'confirm',
|
key: 'confirm',
|
||||||
type: 'icon-button',
|
type: 'icon-button',
|
||||||
label: translator.get('Selection.Confirm'),
|
label: translator.get('Selection.Confirm'),
|
||||||
enabled: () => api.canConfirm(),
|
enabled: () => api.canConfirm(layout),
|
||||||
action: () => {
|
action: () => {
|
||||||
onConfirm();
|
onConfirm();
|
||||||
api.confirm();
|
api.confirm();
|
||||||
|
|||||||
@@ -513,6 +513,7 @@ function nuked(configuration = {}) {
|
|||||||
* @param {FrequencyMode=} [options.frequencyMode=none] Show frequency none|value|percent|relative
|
* @param {FrequencyMode=} [options.frequencyMode=none] Show frequency none|value|percent|relative
|
||||||
* @param {boolean=} [options.histogram=false] Show histogram bar (not applicable for existing objects)
|
* @param {boolean=} [options.histogram=false] Show histogram bar (not applicable for existing objects)
|
||||||
* @param {SearchMode=} [options.search=true] Show the search bar permanently, using the toggle button or when in selection: false|true|toggle
|
* @param {SearchMode=} [options.search=true] Show the search bar permanently, using the toggle button or when in selection: false|true|toggle
|
||||||
|
* @param {boolean=} [options.showLock=false] Show the button for toggling locked state.
|
||||||
* @param {boolean=} [options.toolbar=true] Show the toolbar
|
* @param {boolean=} [options.toolbar=true] Show the toolbar
|
||||||
* @param {boolean=} [options.checkboxes=false] Show values as checkboxes instead of as fields (not applicable for existing objects)
|
* @param {boolean=} [options.checkboxes=false] Show values as checkboxes instead of as fields (not applicable for existing objects)
|
||||||
* @param {boolean=} [options.dense=false] Reduces padding and text size (not applicable for existing objects)
|
* @param {boolean=} [options.dense=false] Reduces padding and text size (not applicable for existing objects)
|
||||||
|
|||||||
@@ -1111,6 +1111,12 @@
|
|||||||
"defaultValue": true,
|
"defaultValue": true,
|
||||||
"type": "#/definitions/SearchMode"
|
"type": "#/definitions/SearchMode"
|
||||||
},
|
},
|
||||||
|
"showLock": {
|
||||||
|
"description": "Show the button for toggling locked state.",
|
||||||
|
"optional": true,
|
||||||
|
"defaultValue": false,
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
"description": "Show the toolbar",
|
"description": "Show the toolbar",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
|
|||||||
1
apis/stardust/types/index.d.ts
vendored
1
apis/stardust/types/index.d.ts
vendored
@@ -339,6 +339,7 @@ declare namespace stardust {
|
|||||||
frequencyMode?: stardust.FrequencyMode;
|
frequencyMode?: stardust.FrequencyMode;
|
||||||
histogram?: boolean;
|
histogram?: boolean;
|
||||||
search?: stardust.SearchMode;
|
search?: stardust.SearchMode;
|
||||||
|
showLock?: boolean;
|
||||||
toolbar?: boolean;
|
toolbar?: boolean;
|
||||||
checkboxes?: boolean;
|
checkboxes?: boolean;
|
||||||
dense?: boolean;
|
dense?: boolean;
|
||||||
|
|||||||
@@ -20,6 +20,18 @@ const StyledGrid = styled(Grid)(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run this on small devices to reset the zoom. Required when focusing
|
||||||
|
* an input field and the browser auto zooms the page. Browsers do not
|
||||||
|
* expose any API for handling this currently.
|
||||||
|
*/
|
||||||
|
function resetZoom() {
|
||||||
|
const viewportMetaTag = document.querySelector('meta[name="viewport"]');
|
||||||
|
if (viewportMetaTag instanceof HTMLMetaElement) {
|
||||||
|
viewportMetaTag.content = 'width=device-width, minimum-scale=1.0, maximum-scale=1.0, initial-scale=1.0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function Search({ onChange = () => {}, onEnter = () => {}, onEscape = () => {} }) {
|
export default function Search({ onChange = () => {}, onEnter = () => {}, onEscape = () => {} }) {
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
@@ -54,6 +66,7 @@ export default function Search({ onChange = () => {}, onEnter = () => {}, onEsca
|
|||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
onFocus={resetZoom}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import SvgIcon from './SvgIcon';
|
|||||||
|
|
||||||
const unlock = (props) => ({
|
const unlock = (props) => ({
|
||||||
...props,
|
...props,
|
||||||
|
viewBox: '0 0 12 16',
|
||||||
shapes: [
|
shapes: [
|
||||||
{
|
{
|
||||||
type: 'path',
|
type: 'path',
|
||||||
@@ -12,3 +13,4 @@ const unlock = (props) => ({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
export default (props) => SvgIcon(unlock(props));
|
export default (props) => SvgIcon(unlock(props));
|
||||||
|
export { unlock };
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const colors = {
|
|||||||
|
|
||||||
grey55: '#8C8C8C',
|
grey55: '#8C8C8C',
|
||||||
|
|
||||||
|
grey45: '#737373',
|
||||||
grey30: '#4D4D4D',
|
grey30: '#4D4D4D',
|
||||||
grey25: '#404040',
|
grey25: '#404040',
|
||||||
grey20: '#333333',
|
grey20: '#333333',
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ const dark = {
|
|||||||
focusBorder: colors.blue,
|
focusBorder: colors.blue,
|
||||||
focusOutline: 'rgba(70, 157, 205, 0.3)',
|
focusOutline: 'rgba(70, 157, 205, 0.3)',
|
||||||
inputBackground: 'rgba(0, 0, 0, 0.2)',
|
inputBackground: 'rgba(0, 0, 0, 0.2)',
|
||||||
|
disabledBackground: colors.grey45,
|
||||||
|
disabledContrastText: colors.grey100,
|
||||||
},
|
},
|
||||||
selected: {
|
selected: {
|
||||||
main: colors.green,
|
main: colors.green,
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ const light = {
|
|||||||
focusBorder: colors.blue,
|
focusBorder: colors.blue,
|
||||||
focusOutline: 'rgba(70, 157, 205, 0.3)',
|
focusOutline: 'rgba(70, 157, 205, 0.3)',
|
||||||
inputBackground: 'rgba(255, 255, 255, 1)',
|
inputBackground: 'rgba(255, 255, 255, 1)',
|
||||||
|
disabledBackground: colors.grey45,
|
||||||
|
disabledContrastText: colors.grey100,
|
||||||
},
|
},
|
||||||
selected: {
|
selected: {
|
||||||
main: colors.green,
|
main: colors.green,
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Reference in New Issue
Block a user