Expanding FileBrowser button widths when page is wide enough.
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Controller Tests / controller-tests (push) Waiting to run
Jest Tests / jest-tests (push) Waiting to run

This commit is contained in:
Roland 2026-07-01 19:52:15 -06:00
parent 7005cd0681
commit 2acdc2e16d
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
4 changed files with 51 additions and 7 deletions

View file

@ -27,7 +27,7 @@ import {useDropzone} from "react-dropzone";
import {deleteFile, ignoreDirectory, makeDirectory, movePaths, renamePath, unignoreDirectory} from "../api";
import _ from 'lodash';
import {SortableTable} from "./SortableTable";
import {useBrowseFiles, useMediaDirectory, useUploadFile, useWROLMode} from "../hooks/customHooks";
import {useBrowseFiles, useElementWidth, useMediaDirectory, useUploadFile, useWROLMode} from "../hooks/customHooks";
import {FileRowTagIcon, FilesRefreshButton} from "./Files";
import {FilePreviewContext} from "./FilePreview";
import {DragSelectionContext, DragSelectionProvider, SettingsContext, ThemeContext} from "../contexts/contexts";
@ -196,6 +196,10 @@ function SkeletonFileRow({depth}) {
);
}
// Below this footer width (px) the action buttons are icon-only; at or above it they expand to show labels.
// The eight labeled buttons need ~1020px to fit on a single row; this leaves a small buffer.
const FOOTER_LABELS_MIN_WIDTH = 1050;
export function FileBrowser() {
const [selectedPaths, setSelectedPaths] = React.useState([]);
const [renameOpen, setRenameOpen] = React.useState(false);
@ -342,9 +346,15 @@ export function FileBrowser() {
}
}
const [footerRef, footerWidth] = useElementWidth();
// Expand icon-only buttons to include labels once the footer is wide enough to fit them.
const showLabels = footerWidth >= FOOTER_LABELS_MIN_WIDTH;
// Semantic UI renders a labeled icon button when `content` and `labelPosition` are set, else icon-only.
const labelProps = (content) => showLabels ? {content, labelPosition: 'left'} : {};
const footerClassName = `sticky-footer ${inverted}`;
const footer = <div className={footerClassName}>
<FilesRefreshButton paths={selectedPaths}/>
const footer = <div className={footerClassName} ref={footerRef}>
<FilesRefreshButton paths={selectedPaths} showLabel={showLabels}/>
<APIButton
icon='trash'
color='red'
@ -353,11 +363,13 @@ export function FileBrowser() {
onClick={onDelete}
disabled={selectedPathsCount === 0}
obeyWROLMode={true}
{...labelProps('Delete')}
/>
<Button icon='text cursor'
color='yellow'
onClick={() => setRenameOpen(true)}
disabled={wrolModeEnabled || selectedPathsCount !== 1}
{...labelProps('Rename')}
/>
{selectedPathsCount === 1 ?
<RenameModal
@ -371,6 +383,7 @@ export function FileBrowser() {
color='teal'
disabled={wrolModeEnabled || selectedPathsCount === 0}
onClick={() => setMoveOpen(true)}
{...labelProps('Move')}
/>
{selectedPathsCount > 0 ?
<MoveModal open={moveOpen}
@ -382,12 +395,13 @@ export function FileBrowser() {
color='blue'
onClick={() => setMakeDirectoryOpen(true)}
disabled={wrolModeEnabled}
style={{paddingLeft: '1em', paddingRight: '0.8em'}}
style={{paddingLeft: '1em', paddingRight: showLabels ? '1em' : '0.8em'}}
>
<IconGroup>
<Icon name='folder'/>
<Icon corner name='add'/>
</IconGroup>
{showLabels ? <>&nbsp;New Folder</> : null}
</Button>
<MakeDirectoryModal
open={makeDirectoryOpen}
@ -400,6 +414,7 @@ export function FileBrowser() {
color='green'
disabled={wrolModeEnabled || (selectedPathsCount > 0 && !singleDirectorySelected)}
onClick={() => setUploadOpen(true)}
{...labelProps('Upload')}
/>
<FileBrowserUploadModal
open={uploadOpen}
@ -412,6 +427,7 @@ export function FileBrowser() {
icon={isDirectoryIgnored ? 'eye' : 'eye slash'}
disabled={!singleDirectorySelected || wrolModeEnabled}
onClick={() => setIgnoreDirectoryOpen(true)}
{...labelProps(isDirectoryIgnored ? 'Unignore' : 'Ignore')}
/>
<IgnoreDirectoryModal
open={ignoreDirectoryOpen}
@ -425,6 +441,7 @@ export function FileBrowser() {
icon='tags'
disabled={selectedPathsCount === 0}
onClick={() => setBulkTagOpen(true)}
{...labelProps('Tag')}
/>
<BulkTagModal
open={bulkTagOpen}

View file

@ -34,6 +34,7 @@ jest.mock('../hooks/customHooks', () => ({
useWROLMode: jest.fn(() => false),
useStatusFlag: jest.fn(() => false),
useUploadFile: () => mockUseUploadFile,
useElementWidth: jest.fn(() => [() => {}, 0]),
}));
// Mock react-dropzone
@ -45,7 +46,7 @@ jest.mock('react-dropzone', () => ({
}));
// Import mocked hooks for manipulation
import {useBrowseFiles, useWROLMode} from '../hooks/customHooks';
import {useBrowseFiles, useElementWidth, useWROLMode} from '../hooks/customHooks';
// Create wrapper with all required contexts
const AllContextsWrapper = ({children}) => {
@ -91,6 +92,8 @@ describe('FileBrowser', () => {
fetchFiles: jest.fn(),
});
useWROLMode.mockReturnValue(false);
// [callbackRef, width]; width 0 keeps footer buttons icon-only in tests.
useElementWidth.mockReturnValue([() => {}, 0]);
});
describe('Directory Selection', () => {

View file

@ -1132,10 +1132,12 @@ const useRefresh = () => {
}
}
export function FilesRefreshButton({paths}) {
export function FilesRefreshButton({paths, showLabel}) {
const {globalRefreshing, loading, wrolModeEnabled, refreshFiles} = useRefresh();
return <Button icon='refresh'
content={showLabel ? 'Refresh' : undefined}
labelPosition={showLabel ? 'left' : undefined}
loading={loading || globalRefreshing}
onClick={() => refreshFiles(paths)}
disabled={wrolModeEnabled || globalRefreshing}/>

View file

@ -1,4 +1,4 @@
import React, {useContext, useEffect, useRef, useState} from "react";
import React, {useCallback, useContext, useEffect, useRef, useState} from "react";
import {
ApiDownError,
createChannel,
@ -83,6 +83,28 @@ export const useIsTouchDevice = () => {
return isTouchDevice;
};
export const useElementWidth = () => {
// Tracks the rendered width (in pixels) of an element. Returns a callback ref to attach to the element
// and its current width. Uses a callback ref (not a plain ref) so it re-attaches if the element mounts
// later (e.g. after data loads), and a ResizeObserver so the width updates as the container/viewport changes.
const [width, setWidth] = useState(0);
const observer = useRef(null);
const ref = useCallback((node) => {
if (observer.current) {
observer.current.disconnect();
observer.current = null;
}
if (node && typeof ResizeObserver === 'function') {
setWidth(node.offsetWidth);
observer.current = new ResizeObserver(() => setWidth(node.offsetWidth));
observer.current.observe(node);
}
}, []);
return [ref, width];
};
export const usePlatformModifier = () => {
// Detects if the user is on macOS to display the correct keyboard shortcut modifier.
const [isMac, setIsMac] = useState(false);