mirror of
https://github.com/qlik-oss/nebula.js.git
synced 2025-12-20 10:19:04 -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",
|
||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||
"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",
|
||||
"comment": "Unlock a selection from listbox"
|
||||
},
|
||||
"SelectionToolbar.ClickToLock": {
|
||||
"value": "Click to lock",
|
||||
"comment": "Lock a selection in selections toolbar"
|
||||
},
|
||||
"Visualization.LayoutError": {
|
||||
"value": "Error",
|
||||
"comment": "Status text shown when a visualization has layout errors"
|
||||
|
||||
@@ -258,5 +258,10 @@
|
||||
"value": "La condición de cálculo no se cumple",
|
||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||
"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",
|
||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||
"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",
|
||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||
"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": "演算実行条件が満たされていません",
|
||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
||||
},
|
||||
"SelectionToolbar.ClickToLock": {
|
||||
"value": "クリックしてロック",
|
||||
"comment": "Lock a selection in selection toolbar",
|
||||
"version": "yOnFI2n2tK+87jTmGHU5vw=="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,5 +258,10 @@
|
||||
"value": "계산 조건이 충족되지 않았습니다.",
|
||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||
"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",
|
||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||
"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",
|
||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||
"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",
|
||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||
"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": "Условие вычисления не выполнено",
|
||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||
"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",
|
||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||
"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",
|
||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||
"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": "不满足计算条件",
|
||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
||||
},
|
||||
"SelectionToolbar.ClickToLock": {
|
||||
"value": "单击以锁定",
|
||||
"comment": "Lock a selection in selection toolbar",
|
||||
"version": "yOnFI2n2tK+87jTmGHU5vw=="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,5 +258,10 @@
|
||||
"value": "不符計算條件",
|
||||
"comment": "Message displayed when a calculation condition is not fulfilled",
|
||||
"version": "Hj3EiDijTWZ+MhDaqSx/uQ=="
|
||||
},
|
||||
"SelectionToolbar.ClickToLock": {
|
||||
"value": "按一下鎖定",
|
||||
"comment": "Lock a selection in selection toolbar",
|
||||
"version": "yOnFI2n2tK+87jTmGHU5vw=="
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ function ActionsToolbar({
|
||||
onConfirm: () => {},
|
||||
onCancel: () => {},
|
||||
},
|
||||
extraItems,
|
||||
more = {
|
||||
enabled: false,
|
||||
actions: [],
|
||||
@@ -78,10 +79,11 @@ function ActionsToolbar({
|
||||
},
|
||||
focusHandler = null,
|
||||
actionsRefMock = null, // for testing
|
||||
direction = 'ltr',
|
||||
isRtl,
|
||||
autoConfirm = false,
|
||||
layout,
|
||||
}) {
|
||||
const defaultSelectionActions = useDefaultSelectionActions(selections);
|
||||
const defaultSelectionActions = useDefaultSelectionActions({ ...selections, layout });
|
||||
|
||||
const { translator, keyboardNavigation } = useContext(InstanceContext);
|
||||
const [showMoreItems, setShowMoreItems] = useState(false);
|
||||
@@ -104,8 +106,8 @@ function ActionsToolbar({
|
||||
};
|
||||
|
||||
const handleActionsKeyDown = useMemo(
|
||||
() => getActionsKeyDownHandler({ keyboardNavigation, focusHandler, getEnabledButton, selections }),
|
||||
[keyboardNavigation, focusHandler, getEnabledButton, selections]
|
||||
() => getActionsKeyDownHandler({ keyboardNavigation, focusHandler, getEnabledButton, selections, isRtl }),
|
||||
[keyboardNavigation, focusHandler, getEnabledButton, selections, isRtl]
|
||||
);
|
||||
|
||||
useEffect(
|
||||
@@ -120,11 +122,11 @@ function ActionsToolbar({
|
||||
|
||||
const focusFirst = () => {
|
||||
const enabledButton = getEnabledButton(false);
|
||||
enabledButton && enabledButton.focus();
|
||||
enabledButton?.focus();
|
||||
};
|
||||
const focusLast = () => {
|
||||
const enabledButton = getEnabledButton(true);
|
||||
enabledButton && enabledButton.focus();
|
||||
enabledButton?.focus();
|
||||
};
|
||||
focusHandler.on('focus_toolbar_first', focusFirst);
|
||||
focusHandler.on('focus_toolbar_last', focusLast);
|
||||
@@ -163,7 +165,6 @@ function ActionsToolbar({
|
||||
const showActions = newActions.length > 0;
|
||||
const showMore = moreActions.length > 0;
|
||||
const showDivider = (showActions && selections.show) || (showMore && selections.show);
|
||||
const isRtl = direction === 'rtl';
|
||||
|
||||
const Actions = (
|
||||
<Grid
|
||||
@@ -176,6 +177,9 @@ function ActionsToolbar({
|
||||
data-testid="actions-toolbar"
|
||||
sx={{ flexDirection: isRtl ? 'row-reverse' : 'row' }}
|
||||
>
|
||||
{extraItems?.length && (
|
||||
<ActionsGroup className="actions-toolbar-extra-actions" actions={extraItems} isRtl={isRtl} />
|
||||
)}
|
||||
{showActions && <ActionsGroup actions={newActions} />}
|
||||
{showMore && (
|
||||
<ActionsGroup
|
||||
|
||||
@@ -89,6 +89,7 @@ function Header({ layout, sn, anchorEl, hovering, focusHandler, titleStyles = {}
|
||||
actions={actions}
|
||||
popover={{ show: showPopoverToolbar, anchorEl }}
|
||||
focusHandler={focusHandler}
|
||||
layout={layout}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -17,7 +17,13 @@ const focusButton = (index) => {
|
||||
btn.focus();
|
||||
};
|
||||
|
||||
export default function getActionsKeyDownHandler({ keyboardNavigation, focusHandler, getEnabledButton, selections }) {
|
||||
export default function getActionsKeyDownHandler({
|
||||
keyboardNavigation,
|
||||
focusHandler,
|
||||
getEnabledButton,
|
||||
selections,
|
||||
isRtl,
|
||||
}) {
|
||||
const handleActionsKeyDown = (evt) => {
|
||||
const { target, nativeEvent } = evt;
|
||||
const { keyCode } = nativeEvent;
|
||||
@@ -29,7 +35,8 @@ export default function getActionsKeyDownHandler({ keyboardNavigation, focusHand
|
||||
const isActionButton = target.classList.contains('njs-cell-action');
|
||||
if (isActionButton) {
|
||||
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);
|
||||
}
|
||||
evt.stopPropagation();
|
||||
|
||||
@@ -2,37 +2,23 @@
|
||||
import React, { useContext, useCallback, useRef, useEffect, useState, useMemo } from 'react';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import Lock from '@nebula.js/ui/icons/lock';
|
||||
import { IconButton, Grid, Typography } from '@mui/material';
|
||||
import { Grid } from '@mui/material';
|
||||
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 ListBox from './ListBox';
|
||||
import createListboxSelectionToolbar from './interactions/listbox-selection-toolbar';
|
||||
import ActionsToolbar from '../ActionsToolbar';
|
||||
import InstanceContext from '../../contexts/InstanceContext';
|
||||
import ListBoxSearch from './components/ListBoxSearch';
|
||||
import getListboxContainerKeyboardNavigation from './interactions/keyboard-navigation/keyboard-nav-container';
|
||||
import getStyles from './assets/styling';
|
||||
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 {
|
||||
CELL_PADDING_LEFT,
|
||||
ICON_WIDTH,
|
||||
ICON_PADDING,
|
||||
BUTTON_ICON_WIDTH,
|
||||
HEADER_PADDING_RIGHT,
|
||||
DENSE_ROW_HEIGHT,
|
||||
SCROLL_BAR_WIDTH,
|
||||
} from './constants';
|
||||
import { DENSE_ROW_HEIGHT, SCROLL_BAR_WIDTH } from './constants';
|
||||
import useTempKeyboard from './components/useTempKeyboard';
|
||||
import ListBoxError from './components/ListBoxError';
|
||||
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';
|
||||
|
||||
const PREFIX = 'ListBoxInline';
|
||||
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();
|
||||
|
||||
function ListBoxInline({ options, layout }) {
|
||||
@@ -111,6 +78,7 @@ function ListBoxInline({ options, layout }) {
|
||||
renderedCallback,
|
||||
toolbar = true,
|
||||
isPopover = false,
|
||||
showLock = false,
|
||||
components,
|
||||
} = options;
|
||||
|
||||
@@ -130,10 +98,6 @@ function ListBoxInline({ options, layout }) {
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const unlock = useCallback(() => {
|
||||
model.unlock('/qListObjectDef');
|
||||
}, [model]);
|
||||
|
||||
const { translator, keyboardNavigation, themeApi, constraints } = useContext(InstanceContext);
|
||||
|
||||
const { checkboxes = checkboxesOption } = layout || {};
|
||||
@@ -152,13 +116,11 @@ function ListBoxInline({ options, layout }) {
|
||||
const updateKeyScroll = (newState) => setKeyScroll((current) => ({ ...current, ...newState }));
|
||||
const [currentScrollIndex, setCurrentScrollIndex] = useState({ start: 0, stop: 0 });
|
||||
const [appSelections] = useAppSelections(app);
|
||||
const titleRef = useRef(null);
|
||||
const [selectionState] = useState(() => createSelectionState());
|
||||
const keyboard = useTempKeyboard({ containerRef, enabled: keyboardNavigation });
|
||||
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 [isToolbarDetached, setIsToolbarDetached] = useState(false);
|
||||
|
||||
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(() => {
|
||||
document.addEventListener('keydown', globalKeyDown);
|
||||
return () => {
|
||||
@@ -197,6 +156,9 @@ function ListBoxInline({ options, layout }) {
|
||||
}, [globalKeyDown]);
|
||||
|
||||
useEffect(() => {
|
||||
if (search === true) {
|
||||
setShowSearch(true);
|
||||
}
|
||||
const show = () => {
|
||||
setShowToolbar(true);
|
||||
};
|
||||
@@ -207,6 +169,7 @@ function ListBoxInline({ options, layout }) {
|
||||
}
|
||||
};
|
||||
if (isPopover) {
|
||||
// When isPopover, toolbar == false will be ignored.
|
||||
if (!selections.isActive()) {
|
||||
selections.begin('/qListObjectDef');
|
||||
selections.on('activated', show);
|
||||
@@ -214,7 +177,7 @@ function ListBoxInline({ options, layout }) {
|
||||
}
|
||||
setShowToolbar(isPopover);
|
||||
}
|
||||
if (selections) {
|
||||
if (toolbar && selections) {
|
||||
if (!selections.isModal()) {
|
||||
selections.on('activated', show);
|
||||
selections.on('deactivated', hide);
|
||||
@@ -228,7 +191,7 @@ function ListBoxInline({ options, layout }) {
|
||||
selections.removeListener('deactivated', hide);
|
||||
}
|
||||
};
|
||||
}, [selections, isPopover]);
|
||||
}, [toolbar, selections, isPopover]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!searchContainer || !searchContainer.current) {
|
||||
@@ -242,75 +205,38 @@ function ListBoxInline({ options, layout }) {
|
||||
}, [searchContainer && searchContainer.current, showSearch, search, focusSearch]);
|
||||
|
||||
const { wildCardSearch, searchEnabled, autoConfirm = false, layoutOptions = {} } = layout ?? {};
|
||||
const showSearchIcon = searchEnabled !== false && search === 'toggle';
|
||||
const isLocked = layout?.qListObject?.qDimensionInfo?.qLocked === true;
|
||||
const showSearchOrLockIcon = isLocked || showSearchIcon;
|
||||
const isLocked = layout?.qListObject?.qDimensionInfo?.qLocked;
|
||||
const showSearchIcon = searchEnabled !== false && search === 'toggle' && !isLocked;
|
||||
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 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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const listboxSelectionToolbarItems = createListboxSelectionToolbar({
|
||||
layout,
|
||||
model,
|
||||
translator,
|
||||
selectionState,
|
||||
isDirectQuery,
|
||||
});
|
||||
|
||||
const showTitle = true;
|
||||
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 onShowSearch = () => {
|
||||
const handleShowSearch = () => {
|
||||
const newValue = !showSearch;
|
||||
setShowSearch(newValue);
|
||||
};
|
||||
|
||||
const onCtrlF = () => {
|
||||
if (search === 'toggle') {
|
||||
onShowSearch();
|
||||
handleShowSearch();
|
||||
} else {
|
||||
const input = searchContainer.current.querySelector('input');
|
||||
input?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const getActionToolbarProps = (isDetached) => ({
|
||||
...getListboxActionProps({
|
||||
isDetached: isPopover ? false : isDetached,
|
||||
showToolbar,
|
||||
containerRef,
|
||||
isLocked,
|
||||
listboxSelectionToolbarItems,
|
||||
selections,
|
||||
keyboard,
|
||||
}),
|
||||
autoConfirm,
|
||||
});
|
||||
|
||||
const shouldAutoFocus = searchVisible && search === 'toggle';
|
||||
|
||||
// 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,
|
||||
});
|
||||
|
||||
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) {
|
||||
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 (
|
||||
<>
|
||||
{showDetachedToolbarOnly && <ActionsToolbar direction={direction} {...getActionToolbarProps(true)} />}
|
||||
{showDetachedToolbarOnly && listBoxHeader}
|
||||
<StyledGrid
|
||||
className="listbox-container"
|
||||
container
|
||||
@@ -374,47 +304,7 @@ function ListBoxInline({ options, layout }) {
|
||||
isGridMode={isGridMode}
|
||||
aria-label={keyboard.active ? translator.get('Listbox.ScreenReaderInstructions') : ''}
|
||||
>
|
||||
{showToolbarWithTitle && (
|
||||
<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>
|
||||
)}
|
||||
{showAttachedToolbar && listBoxHeader}
|
||||
<Grid
|
||||
item
|
||||
container
|
||||
@@ -437,7 +327,7 @@ function ListBoxInline({ options, layout }) {
|
||||
wildCardSearch={wildCardSearch}
|
||||
searchEnabled={searchEnabled}
|
||||
direction={direction}
|
||||
hide={showSearchIcon && onShowSearch}
|
||||
hide={showSearchIcon && handleShowSearch}
|
||||
styles={styles}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@@ -140,8 +140,12 @@ export default function ListBoxPopover({
|
||||
anchorEl={alignTo.current}
|
||||
anchorOrigin={anchorOrigin}
|
||||
transformOrigin={transformOrigin}
|
||||
PaperProps={{
|
||||
style: { minWidth: '250px' },
|
||||
slotProps={{
|
||||
paper: {
|
||||
style: {
|
||||
minWidth: '250px',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Grid container direction="column" gap={0} ref={containerRef}>
|
||||
@@ -160,6 +164,7 @@ export default function ListBoxPopover({
|
||||
<Grid item xs />
|
||||
<Grid item>
|
||||
<ActionsToolbar
|
||||
layout={layout}
|
||||
more={{
|
||||
enabled: !isLocked,
|
||||
actions: listboxSelectionToolbarItems,
|
||||
|
||||
@@ -34,7 +34,6 @@ jest.mock('../interactions/keyboard-navigation/keyboard-nav-container', () => ({
|
||||
describe('<ListboxInline />', () => {
|
||||
const app = { key: 'app' };
|
||||
|
||||
let options;
|
||||
let useState;
|
||||
let useEffect;
|
||||
let useCallback;
|
||||
@@ -49,6 +48,7 @@ describe('<ListboxInline />', () => {
|
||||
let render;
|
||||
let getListboxInlineKeyboardNavigation;
|
||||
let InstanceContext;
|
||||
let defaultOptions;
|
||||
|
||||
beforeEach(() => {
|
||||
useState = jest.fn();
|
||||
@@ -111,7 +111,7 @@ describe('<ListboxInline />', () => {
|
||||
removeListener: jest.fn(),
|
||||
};
|
||||
|
||||
options = {
|
||||
defaultOptions = {
|
||||
app,
|
||||
title: 'title',
|
||||
direction: 'vertical',
|
||||
@@ -161,12 +161,13 @@ describe('<ListboxInline />', () => {
|
||||
beforeEach(() => {
|
||||
const theme = createTheme('dark');
|
||||
|
||||
render = async () => {
|
||||
render = async (options = {}) => {
|
||||
const mergedOptions = { ...defaultOptions, ...options };
|
||||
await act(async () => {
|
||||
renderer = create(
|
||||
<ThemeProvider theme={theme}>
|
||||
<InstanceContext.Provider value={{ translator: { get: (s) => s, language: () => 'sv' } }}>
|
||||
<ListBoxInline options={options} />
|
||||
<ListBoxInline options={mergedOptions} />
|
||||
</InstanceContext.Provider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
@@ -201,8 +202,8 @@ describe('<ListboxInline />', () => {
|
||||
});
|
||||
|
||||
test('should render properly with search toggle option', async () => {
|
||||
options.search = 'toggle';
|
||||
await render();
|
||||
const options = { search: 'toggle' };
|
||||
await render(options);
|
||||
|
||||
const searchToggleBtns = renderer.root
|
||||
.findAllByProps({ 'data-testid': 'search-toggle-btn' })
|
||||
@@ -215,8 +216,8 @@ describe('<ListboxInline />', () => {
|
||||
});
|
||||
|
||||
test('should render without toolbar', async () => {
|
||||
options.toolbar = false;
|
||||
await render();
|
||||
const options = { toolbar: false };
|
||||
await render(options);
|
||||
const actionToolbars = renderer.root.findAllByType(ActionsToolbar);
|
||||
expect(actionToolbars).toHaveLength(0);
|
||||
|
||||
@@ -228,8 +229,8 @@ describe('<ListboxInline />', () => {
|
||||
});
|
||||
|
||||
test('should render without toolbar', async () => {
|
||||
options.search = 'toggle';
|
||||
await render();
|
||||
const options = { search: 'toggle' };
|
||||
await render(options);
|
||||
|
||||
expect(ListBoxSearch.mock.calls[0][0]).toMatchObject({
|
||||
visible: false,
|
||||
@@ -241,8 +242,8 @@ describe('<ListboxInline />', () => {
|
||||
});
|
||||
|
||||
test('should render without search and show search button', async () => {
|
||||
options.search = false;
|
||||
await render();
|
||||
const options = { search: false };
|
||||
await render(options);
|
||||
const actionToolbars = renderer.root.findAllByType(ActionsToolbar);
|
||||
expect(actionToolbars).toHaveLength(1);
|
||||
|
||||
@@ -255,8 +256,8 @@ describe('<ListboxInline />', () => {
|
||||
});
|
||||
|
||||
test('should render with NOT autoFocus when search is true', async () => {
|
||||
options.search = true;
|
||||
await render();
|
||||
const options = { search: true };
|
||||
await render(options);
|
||||
|
||||
expect(ListBoxSearch.mock.calls[0][0]).toMatchObject({
|
||||
visible: true,
|
||||
@@ -265,9 +266,8 @@ describe('<ListboxInline />', () => {
|
||||
});
|
||||
|
||||
test('should show toolbar when opened in a popover', async () => {
|
||||
options.search = false;
|
||||
options.isPopover = true;
|
||||
await render();
|
||||
const options = { search: false, isPopover: true };
|
||||
await render(options);
|
||||
const actionToolbars = renderer.root.findAllByType(ActionsToolbar);
|
||||
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,
|
||||
paddingRight: checkboxes ? 0 : isRtl ? 8 : 0,
|
||||
justifyContent: valueTextAlign,
|
||||
textAlign: valueTextAlign,
|
||||
};
|
||||
|
||||
const isFirstElement = index === 0;
|
||||
|
||||
const showLock = isSelected && isLocked;
|
||||
const showTick = !checkboxes && isSelected && !isLocked;
|
||||
const showIcon = !checkboxes && sizePermitsTickOrLock;
|
||||
const showLockIcon = isSelected && isLocked;
|
||||
const showTickIcon = !checkboxes && isSelected && !isLocked;
|
||||
const showAnyIcon = !checkboxes && sizePermitsTickOrLock;
|
||||
const cellPaddingRight = checkboxes || !sizePermitsTickOrLock;
|
||||
|
||||
const ariaLabel = getValueLabel({
|
||||
@@ -288,10 +289,10 @@ function RowColumn({ index, rowIndex, columnIndex, style, data }) {
|
||||
|
||||
{freqIsAllowed && <Frequency cell={cell} checkboxes={checkboxes} dense={dense} showGray={showGray} />}
|
||||
|
||||
{showIcon && (
|
||||
{showAnyIcon && (
|
||||
<Grid item className={classes.icon}>
|
||||
{showLock && <Lock style={iconStyles} size="small" />}
|
||||
{showTick && <Tick style={iconStyles} size="small" />}
|
||||
{showLockIcon && <Lock style={iconStyles} size="small" />}
|
||||
{showTickIcon && <Tick style={iconStyles} size="small" />}
|
||||
</Grid>
|
||||
)}
|
||||
</ItemGrid>
|
||||
|
||||
@@ -154,7 +154,7 @@ export default function ListBoxSearch({
|
||||
|
||||
function focusRow(container) {
|
||||
const row = container?.querySelector('.last-focused') || container?.querySelector('[role="row"]:first-child');
|
||||
row.setAttribute('tabIndex', 0);
|
||||
row?.setAttribute('tabIndex', 0);
|
||||
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 firstRowElement = c?.querySelector('.value.selector, .value');
|
||||
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?.focus();
|
||||
},
|
||||
focusSelection() {
|
||||
const unlockCoverButton = document.querySelector('#listbox-unlock-button');
|
||||
const confirmButton = document.querySelector('.actions-toolbar-default-actions .actions-toolbar-confirm');
|
||||
confirmButton?.setAttribute('tabIndex', 0);
|
||||
confirmButton?.focus();
|
||||
const btnToFocus = unlockCoverButton || confirmButton;
|
||||
btnToFocus?.setAttribute('tabIndex', 0);
|
||||
btnToFocus?.focus();
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import showToolbarDetached from '../listbox-show-toolbar-detached';
|
||||
|
||||
const iconsWidth = 28;
|
||||
const headerPaddingLeft = 9;
|
||||
const headerPaddingRight = 4;
|
||||
const paddingLeft = 9;
|
||||
const paddingRight = 4;
|
||||
|
||||
describe('show listbox toolbar detached', () => {
|
||||
it('should return true if there is not enough space for toolbar', () => {
|
||||
const containerRect = { width: 100 };
|
||||
const titleRef = { current: { clientWidth: 50, scrollWidth: 80, offsetWidth: 81 } };
|
||||
expect(
|
||||
showToolbarDetached({ containerRect, titleRef, iconsWidth, headerPaddingLeft, headerPaddingRight })
|
||||
).toStrictEqual(true);
|
||||
expect(showToolbarDetached({ containerRect, titleRef, iconsWidth, paddingLeft, paddingRight })).toStrictEqual(true);
|
||||
});
|
||||
|
||||
it('should return true if title is truncated', () => {
|
||||
const containerRect = { width: 300 };
|
||||
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);
|
||||
});
|
||||
@@ -24,7 +22,7 @@ describe('show listbox toolbar detached', () => {
|
||||
it('should return false if there is enough space for title and toolbar', () => {
|
||||
const containerRect = { width: 300 };
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -4,15 +4,18 @@ export default function getListboxActionProps({
|
||||
containerRef,
|
||||
isLocked,
|
||||
listboxSelectionToolbarItems,
|
||||
extraItems,
|
||||
selections,
|
||||
keyboard,
|
||||
}) {
|
||||
return {
|
||||
isDetached,
|
||||
show: showToolbar && !isDetached,
|
||||
popover: {
|
||||
show: showToolbar && isDetached,
|
||||
anchorEl: containerRef.current,
|
||||
},
|
||||
extraItems,
|
||||
more: {
|
||||
enabled: !isLocked,
|
||||
actions: listboxSelectionToolbarItems,
|
||||
@@ -1,18 +1,11 @@
|
||||
export default function showToolbarDetached({
|
||||
containerRect,
|
||||
titleRef,
|
||||
iconsWidth,
|
||||
headerPaddingLeft,
|
||||
headerPaddingRight,
|
||||
}) {
|
||||
export default function showToolbarDetached({ containerRect, titleRef, iconsWidth, paddingLeft, paddingRight }) {
|
||||
const containerWidth = containerRect.width;
|
||||
const preventTruncation = 2;
|
||||
const padding = headerPaddingLeft + headerPaddingRight;
|
||||
const padding = paddingLeft + paddingRight;
|
||||
const contentWidth = (titleRef?.current?.clientWidth ?? 0) + iconsWidth + padding + preventTruncation;
|
||||
const actionToolbarWidth = 128;
|
||||
const notSufficientSpace = containerWidth < contentWidth + actionToolbarWidth;
|
||||
const isTruncated = titleRef?.current?.scrollWidth > titleRef?.current?.offsetWidth;
|
||||
const isDetached = !!(notSufficientSpace || isTruncated);
|
||||
|
||||
return isDetached;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import InstanceContext from '../contexts/InstanceContext';
|
||||
|
||||
export default function useDefaultSelectionActions({
|
||||
api,
|
||||
layout,
|
||||
onConfirm = () => {},
|
||||
onCancel = () => {},
|
||||
onKeyDeactivate = () => {},
|
||||
@@ -17,7 +18,7 @@ export default function useDefaultSelectionActions({
|
||||
key: 'clear',
|
||||
type: 'icon-button',
|
||||
label: translator.get('Selection.Clear'),
|
||||
enabled: () => api.canClear(),
|
||||
enabled: () => api.canClear(layout),
|
||||
action: () => api.clear(),
|
||||
getSvgIconShape: clearSelections,
|
||||
},
|
||||
@@ -25,7 +26,7 @@ export default function useDefaultSelectionActions({
|
||||
key: 'cancel',
|
||||
type: 'icon-button',
|
||||
label: translator.get('Selection.Cancel'),
|
||||
enabled: () => api.canCancel(),
|
||||
enabled: () => api.canCancel(layout),
|
||||
action: () => {
|
||||
onCancel();
|
||||
api.cancel();
|
||||
@@ -41,7 +42,7 @@ export default function useDefaultSelectionActions({
|
||||
key: 'confirm',
|
||||
type: 'icon-button',
|
||||
label: translator.get('Selection.Confirm'),
|
||||
enabled: () => api.canConfirm(),
|
||||
enabled: () => api.canConfirm(layout),
|
||||
action: () => {
|
||||
onConfirm();
|
||||
api.confirm();
|
||||
|
||||
@@ -513,6 +513,7 @@ function nuked(configuration = {}) {
|
||||
* @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 {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.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)
|
||||
|
||||
@@ -1111,6 +1111,12 @@
|
||||
"defaultValue": true,
|
||||
"type": "#/definitions/SearchMode"
|
||||
},
|
||||
"showLock": {
|
||||
"description": "Show the button for toggling locked state.",
|
||||
"optional": true,
|
||||
"defaultValue": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"toolbar": {
|
||||
"description": "Show the toolbar",
|
||||
"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;
|
||||
histogram?: boolean;
|
||||
search?: stardust.SearchMode;
|
||||
showLock?: boolean;
|
||||
toolbar?: boolean;
|
||||
checkboxes?: 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 = () => {} }) {
|
||||
const [value, setValue] = useState('');
|
||||
const handleChange = (e) => {
|
||||
@@ -54,6 +66,7 @@ export default function Search({ onChange = () => {}, onEnter = () => {}, onEsca
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onFocus={resetZoom}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@@ -2,6 +2,7 @@ import SvgIcon from './SvgIcon';
|
||||
|
||||
const unlock = (props) => ({
|
||||
...props,
|
||||
viewBox: '0 0 12 16',
|
||||
shapes: [
|
||||
{
|
||||
type: 'path',
|
||||
@@ -12,3 +13,4 @@ const unlock = (props) => ({
|
||||
],
|
||||
});
|
||||
export default (props) => SvgIcon(unlock(props));
|
||||
export { unlock };
|
||||
|
||||
@@ -15,6 +15,7 @@ const colors = {
|
||||
|
||||
grey55: '#8C8C8C',
|
||||
|
||||
grey45: '#737373',
|
||||
grey30: '#4D4D4D',
|
||||
grey25: '#404040',
|
||||
grey20: '#333333',
|
||||
|
||||
@@ -43,6 +43,8 @@ const dark = {
|
||||
focusBorder: colors.blue,
|
||||
focusOutline: 'rgba(70, 157, 205, 0.3)',
|
||||
inputBackground: 'rgba(0, 0, 0, 0.2)',
|
||||
disabledBackground: colors.grey45,
|
||||
disabledContrastText: colors.grey100,
|
||||
},
|
||||
selected: {
|
||||
main: colors.green,
|
||||
|
||||
@@ -41,6 +41,8 @@ const light = {
|
||||
focusBorder: colors.blue,
|
||||
focusOutline: 'rgba(70, 157, 205, 0.3)',
|
||||
inputBackground: 'rgba(255, 255, 255, 1)',
|
||||
disabledBackground: colors.grey45,
|
||||
disabledContrastText: colors.grey100,
|
||||
},
|
||||
selected: {
|
||||
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