Skip to content

Commit

Permalink
Add option to override UI density. #169 #564
Browse files Browse the repository at this point in the history
Also replaced color scheme selector with a
settings modal that includes both options.
  • Loading branch information
tnajdek committed Sep 25, 2024
1 parent 577d5c4 commit bdbbf3c
Show file tree
Hide file tree
Showing 24 changed files with 230 additions and 111 deletions.
6 changes: 4 additions & 2 deletions src/js/component/modal-manager.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ import NewCollectionModal from './modal/new-collection';
import NewFileModal from './modal/new-file';
import NewItemModal from './modal/new-item';
import RenameCollectionModal from './modal/rename-collection';
import SettingsModal from './modal/settings';
import StyleInstallerModal from './modal/style-installer';
import IdentifierPicker from './modal/identifier-picker';
import ManageTagsModal from './modal/manage-tags';

import { ADD_LINKED_URL_TOUCH, BIBLIOGRAPHY, COLLECTION_ADD, COLLECTION_RENAME, COLLECTION_SELECT,
EXPORT, IDENTIFIER_PICKER, MANAGE_TAGS, MOVE_COLLECTION, NEW_ITEM, SORT_ITEMS, STYLE_INSTALLER,
ADD_BY_IDENTIFIER, NEW_FILE } from '../constants/modals';
EXPORT, IDENTIFIER_PICKER, MANAGE_TAGS, MOVE_COLLECTION, NEW_ITEM, SETTINGS, SORT_ITEMS,
STYLE_INSTALLER, ADD_BY_IDENTIFIER, NEW_FILE } from '../constants/modals';

const lookup = {
[ADD_BY_IDENTIFIER]: AddByIdentifierModal,
Expand All @@ -36,6 +37,7 @@ const lookup = {
[NEW_ITEM]: NewItemModal,
[SORT_ITEMS]: ItemsSortModal,
[STYLE_INSTALLER]: StyleInstallerModal,
[SETTINGS]: SettingsModal,
};

const UNMOUNT_DELAY = 500; // to allow outro animatons (delay in ms)
Expand Down
141 changes: 141 additions & 0 deletions src/js/component/modal/settings.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import cx from 'classnames';
import { memo, useCallback, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Button, Icon } from 'web-common/components';

import Modal from '../ui/modal';
import { preferenceChange, toggleModal } from '../../actions';
import { SETTINGS } from '../../constants/modals';
import Select from '../form/select';
import { getUniqueId } from '../../utils';

const colorSchemeOptions = [
{ label: 'Automatic', value: '' },
{ label: 'Light', value: 'light' },
{ label: 'Dark', value: 'dark' },
];

const densityOptions = [
{ label: 'Automatic', value: '' },
{ label: 'Mouse', value: 'mouse' },
{ label: 'Touch', value: 'touch' },
];

const SettingsModal = () => {
const dispatch = useDispatch();
const colorScheme = useSelector(state => state.preferences.colorScheme);
const density = useSelector(state => state.preferences.density);
const useDarkModeForContent = useSelector(state => state.preferences.useDarkModeForContent);
const isSmall = useSelector(state => state.device.xxs || state.device.xs || state.device.sm);
const isOpen = useSelector(state => state.modal.id === SETTINGS);
const colorSchemeInputId = useRef(getUniqueId());
const densityInputId = useRef(getUniqueId());
const useDarkModeForContentInputId = useRef(getUniqueId());

console.log({ isSmall });

const handleChange = useCallback(() => true, []);

const handleSelectColorScheme = useCallback((newColorScheme) => {
dispatch(preferenceChange('colorScheme', newColorScheme));
}, [dispatch]);

const handleSelectDensity = useCallback((newDensity) => {
dispatch(preferenceChange('density', newDensity));
}, [dispatch]);

const handleUseDarkModeForContentChange = useCallback((ev) => {
dispatch(preferenceChange('useDarkModeForContent', ev.target.checked));
}, [dispatch]);

const handleClose = useCallback(
() => dispatch(toggleModal(SETTINGS, false)),
[dispatch]);

return (
<Modal
className="modal-touch modal-settings"
contentLabel="Settings"
isOpen={isOpen}
onRequestClose={handleClose}
overlayClassName="modal-centered modal-slide"
>
<div className="modal-header">
<div className="modal-header-left">
</div>
<div className="modal-header-center">
<h4 className="modal-title truncate">
Settings
</h4>
</div>
<div className="modal-header-right">
<Button
icon
className="close"
onClick={handleClose}
>
<Icon type={'16/close'} width="16" height="16" />
</Button>
</div>
</div>
<div className="modal-body">
<div className="form">
<div className={cx("form-group", { disabled: isSmall })}>
<label
className="col-form-label"
htmlFor={densityInputId.current}
>
UI Density
</label>
<div className="col">
<Select
isDisabled={isSmall}
id={densityInputId.current}
className="form-control form-control-sm"
onChange={handleChange}
onCommit={handleSelectDensity}
options={densityOptions}
value={isSmall ? 'touch' : density}
searchable={true}
/>
</div>
</div>
<div className="form-group">
<label
className="col-form-label"
htmlFor={colorSchemeInputId.current}
>
Color Scheme
</label>
<div className="col">
<Select
id={colorSchemeInputId.current}
className="form-control form-control-sm"
onChange={handleChange}
onCommit={handleSelectColorScheme}
options={colorSchemeOptions}
value={colorScheme}
searchable={true}
/>
</div>
</div>
<div className={cx("form-group checkbox", { disabled: colorScheme === 'light' })}>
<input
checked={colorScheme !== 'light' && useDarkModeForContent}
className="col-form-label"
disabled={colorScheme === 'light'}
id={useDarkModeForContentInputId.current}
onChange={handleUseDarkModeForContentChange}
type="checkbox"
/>
<label htmlFor={useDarkModeForContentInputId.current}>
Use Dark Mode for Content
</label>
</div>
</div>
</div>
</Modal>
);
}

export default memo(SettingsModal);
86 changes: 16 additions & 70 deletions src/js/component/ui/navbar.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import PropTypes from 'prop-types';
import cx from 'classnames';
import { Fragment, memo, useCallback, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Button, DropdownToggle, DropdownMenu, DropdownItem, Icon, UncontrolledDropdown } from 'web-common/components';
import { Button, Icon } from 'web-common/components';
import { useFocusManager } from 'web-common/hooks';
import { isTriggerEvent } from 'web-common/utils';

import MenuEntry from './menu-entry';
import Search from './../../component/search';
import { currentTriggerSearchMode, preferenceChange, toggleNavbar, toggleTouchTagSelector } from '../../actions';
import { SETTINGS } from '../../constants/modals';
import { currentTriggerSearchMode, toggleModal,
toggleNavbar, toggleTouchTagSelector } from '../../actions';

const Navbar = memo(({ entries = [] }) => {
const ref = useRef(null);
Expand All @@ -18,8 +19,6 @@ const Navbar = memo(({ entries = [] }) => {
const isLibrariesView = useSelector(state => state.current.view === 'libraries');
const isSingleColumn = useSelector(state => state.device.isSingleColumn);
const colorScheme = useSelector(state => state.preferences.colorScheme);
// useDarkModeForContent === null means enabled
const useDarkModeForContent = useSelector(state => state.preferences.useDarkModeForContent) ?? true;

const handleSearchButtonClick = useCallback(() => {
dispatch(currentTriggerSearchMode());
Expand All @@ -29,16 +28,6 @@ const Navbar = memo(({ entries = [] }) => {
dispatch(toggleTouchTagSelector());
}, [dispatch]);

const handleSelectColorScheme = useCallback((ev) => {
const colorScheme = ev.target.dataset.colorScheme === 'automatic'
? null : ev.target.dataset.colorScheme;
dispatch(preferenceChange('colorScheme', colorScheme));
}, [dispatch]);

const handleToggleUseDarkModeForContent = useCallback(() => {
dispatch(preferenceChange('useDarkModeForContent', !useDarkModeForContent));
}, [dispatch, useDarkModeForContent]);

const handleKeyDown = useCallback(ev => {
if(ev.target !== ev.currentTarget) {
return;
Expand All @@ -55,6 +44,10 @@ const Navbar = memo(({ entries = [] }) => {
}
}, [focusNext, focusPrev]);

const handleSettingsButtonClick = useCallback(() => {
dispatch(toggleModal(SETTINGS, true));
}, [dispatch]);

const handleNavbarToggle = useCallback(() => {
dispatch(toggleNavbar(null));
}, [dispatch]);
Expand Down Expand Up @@ -122,63 +115,16 @@ const Navbar = memo(({ entries = [] }) => {
</Button>
</Fragment>
) }
<UncontrolledDropdown className="color-scheme-dropdown">
<DropdownToggle
color={null}
className="btn-icon dropdown-toggle nav-link"
<Button
aria-label="Open Settings"
className="settings-toggle"
icon
onClick={ handleSettingsButtonClick }
onKeyDown={handleKeyDown}
tabIndex={-2}
title="Color Scheme"
>
<Icon
type={'32/color-scheme'}
useColorScheme={ true }
colorScheme={ colorScheme }
width="24"
height="24"
/>
<Icon type="16/chevron-9" width="16" height="16" />
</DropdownToggle>
<DropdownMenu>
<DropdownItem
role="menuitemcheckbox"
aria-checked={!colorScheme }
data-color-scheme="automatic"
onClick={ handleSelectColorScheme }
>
<span className="tick">{ !colorScheme ? "✓" : ""}</span>
Automatic
</DropdownItem>
<DropdownItem
role="menuitemcheckbox"
aria-checked={ colorScheme === 'light' }
data-color-scheme="light"
onClick={ handleSelectColorScheme }
>
<span className="tick">{ colorScheme === 'light' ? "✓" : "" }</span>
Light
</DropdownItem>
<DropdownItem
role="menuitemcheckbox"
aria-checked={ colorScheme === 'dark' }
data-color-scheme="dark"
onClick={ handleSelectColorScheme }
>
<span className="tick">{ colorScheme === 'dark' ? "✓" : "" }</span>
Dark
</DropdownItem>
<DropdownItem divider />
<DropdownItem
role="menuitemcheckbox"
aria-checked={ useDarkModeForContent }
onClick={ handleToggleUseDarkModeForContent }
disabled={ colorScheme === 'light' }
className={ cx({ disabled: colorScheme === 'light' })}
>
<span className="tick">{useDarkModeForContent ? "✓" : ""}</span>
Use Dark Mode for content
</DropdownItem>
</DropdownMenu>
</UncontrolledDropdown>
<Icon type={'16/cog'} width="24" height="24" />
</Button>
<Button
icon
data-navbar-toggle
Expand Down
1 change: 1 addition & 0 deletions src/js/constants/modals.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export const ADD_LINKED_URL_TOUCH = 'ADD_LINKED_URL_TOUCH';
export const IDENTIFIER_PICKER = 'IDENTIFIER_PICKER';
export const MANAGE_TAGS = 'MANAGE_TAGS';
export const EMBEDDED_LIBRARIES_TREE = 'EMBEDDED_LIBRARIES_TREE';
export const SETTINGS = 'SETTINGS';
4 changes: 2 additions & 2 deletions src/js/reducers/current.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const getLibraryKey = (params, config) => {
}


const current = (state = stateDefault, action, { config = {}, device = {} } = {}) => {
const current = (state = stateDefault, action, { config = {}, device = {}, preferences = {} } = {}) => {
switch(action.type) {
case CONFIGURE:
return {
Expand Down Expand Up @@ -210,7 +210,7 @@ const current = (state = stateDefault, action, { config = {}, device = {} } = {}
case TRIGGER_USER_TYPE_CHANGE:
return {
...state,
editingItemKey: action.userType === 'mouse' && !device.xxs && !device.xs &&
editingItemKey: (preferences.density ? preferences.density : action.userType) === 'mouse' && !device.xxs && !device.xs &&
!device.sm && !device.md ? null : state.editingItemKey
}
case TRIGGER_FOCUS:
Expand Down
39 changes: 31 additions & 8 deletions src/js/reducers/device.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TRIGGER_USER_TYPE_CHANGE, TRIGGER_RESIZE_VIEWPORT } from '../constants/actions.js';
import { TRIGGER_USER_TYPE_CHANGE, TRIGGER_RESIZE_VIEWPORT, PREFERENCE_CHANGE } from '../constants/actions.js';
import { getScrollbarWidth, pick } from 'web-common/utils';

const isInitiallyMouse = typeof(matchMedia) === 'function' ? matchMedia('(pointer:fine)').matches : null;
Expand Down Expand Up @@ -41,19 +41,42 @@ const getDevice = (userType, viewport, { isEmbedded } = {}) => {
shouldUseSidebar, shouldUseTabs };
};

const device = (state = defaultState, action, { config } = {}) => {
var viewport;
const getUserType = (state, action, preferences) => {
if (action.type === PREFERENCE_CHANGE && action.name === 'density') {
return action.value ? action.value : state.lastDetectedUserType;
}

if (preferences.density) {
return preferences.density;
}

return state.userType;
}

const getUserTypeBooleans = (state, action, userType) => {
return {
isKeyboardUser: action.type === TRIGGER_USER_TYPE_CHANGE ? action.isKeyboardUser : state.isKeyboardUser,
isMouseUser: userType === 'mouse',
isTouchUser: userType === 'touch'
};
}

const device = (state = defaultState, action, { config, preferences } = {}) => {
switch(action.type) {
case PREFERENCE_CHANGE:
case TRIGGER_RESIZE_VIEWPORT:
case TRIGGER_USER_TYPE_CHANGE:
viewport = action.type === TRIGGER_RESIZE_VIEWPORT ? getViewport(action) : pick(state, ['xxs', 'xs', 'sm', 'md', 'lg']);
case TRIGGER_USER_TYPE_CHANGE: {
const viewport = action.type === TRIGGER_RESIZE_VIEWPORT ? getViewport(action) : pick(state, ['xxs', 'xs', 'sm', 'md', 'lg']);
const userType = getUserType(state, action, preferences);
return {
...state,
...pick(action, ['isKeyboardUser', 'isMouseUser', 'isTouchUser', 'userType']),
...getDevice('userType' in action ? action.userType : state.userType, viewport, config),
...getUserTypeBooleans(state, action, userType),
...getDevice(userType, viewport, config),
...viewport,
scrollbarWidth: state.userType === 'touch' && action.userType === 'mouse' ? getScrollbarWidth() : state.scrollbarWidth
lastDetectedUserType: action.type === TRIGGER_USER_TYPE_CHANGE ? action.userType : state.lastDetectedUserType,
scrollbarWidth: state.userType === 'touch' && userType === 'mouse' ? getScrollbarWidth() : state.scrollbarWidth
}
}
default:
return state;
}
Expand Down
Loading

0 comments on commit bdbbf3c

Please sign in to comment.