Skip to content

Commit c1023b9

Browse files
authored
RSDEV-277 Improve breadcrumbs on new Gallery page (#61)
This change alters how the breadcrumbs and other buttons and menus at the top of the new gallery page are rendered. The breadcrumbs, a series of links allowing the users to navigate back up the folder hierarchy, is now rendered within a horizontally scrolling box. When tapped, it transforms into a read-only text field that allows users to select and copy the path, which would not otherwise be possible. Keyboard controls are fully supported, with a roving tab index employed for moving the focus between the links and tab correctly triggering the read-only text field. Being able to drag-and-drop onto the breadcrumbs is retained, with the animated added whilst dragging to make it clear which UI elements are dropzones. The remaining buttons and menus -- actions, sorting, and views -- have been re-arranged to be consistent with the other newly designed pages (e.g. sysadmin users page) where the primary action in placed in the top left whilst the other buttons and menus are placed in the top right corner. Also similar to the sysadmin users page, a label describing what is selected is also added by this change. Finally, react-router is used to synchronise the URL search parameter of `mediaType` with the selected gallery section. This means that the new gallery remains backwards compatible with links to the various gallery sections of the old gallery and when `/newGallery` becomes `/gallery` all those existing links will continue to work. I attempted to extended this further by capturing the entire breadcrumb in the URL so that users could use the browser back and forwards buttons to navigate around in addition to the breadcrumb links but this would require some backend changes as the current API endpoints require a folder id to get the listing of a folder's contents; the folder's path is not sufficient given that two folders distinct may have the same name, and thus same path.
1 parent 93a5160 commit c1023b9

13 files changed

+568
-246
lines changed

src/main/webapp/WEB-INF/decorators.xml

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
<pattern>*/public/inventory/**</pattern>
3030
<pattern>/apps</pattern>
3131
<pattern>/newGallery</pattern>
32+
<pattern>/newGallery*</pattern>
33+
<pattern>/newGallery/**</pattern>
3234
</excludes>
3335
<decorator name="public" page="externalPages.jsp">
3436
<pattern>*/signup*</pattern>

src/main/webapp/WEB-INF/urlrewrite.xml

+4
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@
140140
<from>/newGallery</from>
141141
<to>/eln/gallery.html</to>
142142
</rule>
143+
<rule>
144+
<from>/newGallery/**</from>
145+
<to>/eln/gallery.html</to>
146+
</rule>
143147

144148
<!-- Backward compatibility: nextcloud/owncloud were not using /apps prefix until RSDEV-105 -->
145149
<rule>

src/main/webapp/ui/flow-typed/mui.js

+8
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ declare module "@mui/system" {
2424
*/
2525
declare export function darken(color: string, coefficient: number): string;
2626

27+
/**
28+
* Lightens a color.
29+
* @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla(), color()
30+
* @param {number} coefficient - multiplier in the range 0 - 1
31+
* @returns {string} A CSS color string. Hex input values are returned as rgb
32+
*/
33+
declare export function lighten(color: string, coefficient: number): string;
34+
2735
/**
2836
* Applies a transparency to a color.
2937
* @param {string} color - CSS color, i.e. one of: #nnn, #nnnnnn, rgb(), rgba(), hsl(), hsla(), color()

src/main/webapp/ui/src/accentedTheme.js

+24-13
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { createTheme } from "@mui/material";
44
import baseTheme from "./theme";
55
import { mergeThemes } from "./util/styles";
6-
import { darken, alpha } from "@mui/system";
6+
import { darken, alpha, lighten } from "@mui/system";
77
import { toolbarClasses } from "@mui/material/Toolbar";
88
import { typographyClasses } from "@mui/material/Typography";
99
import { svgIconClasses } from "@mui/material/SvgIcon";
@@ -94,11 +94,17 @@ export default function createAccentedTheme(accent: AccentColor): { ... } {
9494
const mainAccentColor = prefersMoreContrast
9595
? "rgb(0,0,0)"
9696
: `hsl(${accent.main.hue}deg, ${accent.main.saturation}%, ${accent.main.lightness}%)`;
97-
const disabledColor = `hsl(${accent.main.hue}deg, 10%, 86%)`;
97+
const disabledColor = lighten(
98+
`hsl(${accent.main.hue}deg, 10%, ${accent.main.lightness}%)`,
99+
0.5
100+
);
98101

99102
const linkButtonText = prefersMoreContrast
100103
? "rgb(0,0,0)"
101-
: `hsl(${accent.main.hue}deg, ${accent.main.saturation}%, 40%)`;
104+
: darken(
105+
`hsl(${accent.main.hue}deg, ${accent.main.saturation}%, ${accent.main.lightness}%)`,
106+
0.5
107+
);
102108

103109
/**
104110
* A background colour that can be used behind headers, toolbars, and other
@@ -211,14 +217,18 @@ export default function createAccentedTheme(accent: AccentColor): { ... } {
211217
[`&:has(.${inputAdornmentClasses.positionStart})`]: {
212218
paddingLeft: 0,
213219
},
214-
[`& .${svgIconClasses.root}`]: {
215-
fill: prefersMoreContrast
216-
? "rgb(0,0,0)"
217-
: contrastTextColor,
220+
[`& .${inputAdornmentClasses.root}`]: {
221+
paddingLeft: baseTheme.spacing(1),
222+
paddingRight: baseTheme.spacing(1),
223+
[`& .${svgIconClasses.root}`]: {
224+
fill: prefersMoreContrast
225+
? "rgb(0,0,0)"
226+
: contrastTextColor,
227+
},
218228
},
219229
"& input": {
220230
padding: baseTheme.spacing(0.5),
221-
paddingLeft: 0,
231+
paddingLeft: baseTheme.spacing(1),
222232
color: prefersMoreContrast
223233
? "rgb(0,0,0)"
224234
: contrastTextColor,
@@ -452,19 +462,20 @@ export default function createAccentedTheme(accent: AccentColor): { ... } {
452462
[`& .${outlinedInputClasses.input}`]: {
453463
paddingTop: "5px",
454464
paddingBottom: "5px",
465+
paddingLeft: baseTheme.spacing(1.5),
455466
},
456467
},
457468
[`& .${inputAdornmentClasses.root}`]: {
458469
height: "100%",
459-
paddingLeft: baseTheme.spacing(1),
460-
paddingRight: baseTheme.spacing(1),
470+
paddingLeft: baseTheme.spacing(1.5),
471+
paddingRight: baseTheme.spacing(1.5),
461472
borderRight: accentedBorder,
462-
backgroundColor: lighterInteractiveColor,
473+
marginRight: 0,
463474
[`& .${typographyClasses.root}`]: {
464475
textTransform: "uppercase",
465476
fontWeight: 700,
466-
fontSize: "0.9rem",
467-
lineHeight: "31px",
477+
fontSize: "0.8125rem",
478+
lineHeight: "20px",
468479
},
469480
},
470481
},

src/main/webapp/ui/src/components/useVerticalRovingTabIndex.js src/main/webapp/ui/src/components/useOneDimensionalRovingTabIndex.js

+20-7
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ import { modulo } from "../util/Util";
1212
* than tabbing through them all.
1313
*
1414
* This custom hook provides an abstraction over implementing a roving tab
15-
* index for a vertical list of elements, so that the up and down arrow keys
16-
* provide navigation between the elements in the list. Tab and shift-tab move
17-
* the user's focus out of the list, and when they return the focus resumes on
18-
* the last element in the list that had focus.
15+
* index for a one-dimensional list of elements, so that either the up and down
16+
* arrow keys provide navigation between the elements in the list, or the left
17+
* and right arrow keys dow. Tab and shift-tab move the user's focus out of the
18+
* list, and when they return the focus resumes on the last element in the list
19+
* that had focus.
1920
*
2021
* There are two parts to how this custom hook is to be used:
2122
*
@@ -31,14 +32,22 @@ import { modulo } from "../util/Util";
3132
* - https://www.w3.org/WAI/ARIA/apg/patterns/radio/examples/radio/
3233
* - https://www.youtube.com/watch?v=uCIC2LNt0bk
3334
*/
34-
export default function useVerticalRovingTabIndex<RefComponent: HTMLElement>({
35+
export default function useOneDimensionalRovingTabIndex<
36+
RefComponent: HTMLElement
37+
>({
3538
max,
39+
direction = "column",
3640
}: {|
3741
/**
3842
* The index of the last element of the vertical list, where the indexing is
3943
* 0-based.
4044
*/
4145
max: number,
46+
47+
/**
48+
* The dimension in which the elements of the list are laid out. Defaults to "column"
49+
*/
50+
direction?: "row" | "column",
4251
|}): {|
4352
/**
4453
* The set of the event handlers that must be attached to the container
@@ -92,9 +101,13 @@ export default function useVerticalRovingTabIndex<RefComponent: HTMLElement>({
92101
* By using modulo rather than min and max the user's focus wraps around
93102
* when reaching the end.
94103
*/
95-
if (e.key === "ArrowUp") {
104+
if (e.key === "ArrowUp" && direction === "column") {
105+
setRovingTabIndex(modulo(rovingTabIndex - 1, max + 1));
106+
} else if (e.key === "ArrowDown" && direction === "column") {
107+
setRovingTabIndex(modulo(rovingTabIndex + 1, max + 1));
108+
} else if (e.key === "ArrowLeft" && direction === "row") {
96109
setRovingTabIndex(modulo(rovingTabIndex - 1, max + 1));
97-
} else if (e.key === "ArrowDown") {
110+
} else if (e.key === "ArrowRight" && direction === "row") {
98111
setRovingTabIndex(modulo(rovingTabIndex + 1, max + 1));
99112
}
100113
}

src/main/webapp/ui/src/eln/gallery/common.js

+38-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
//@flow
22

33
import { COLORS as baseThemeColors } from "../../theme";
4+
import Result from "../../util/result";
5+
import * as Parsers from "../../util/parsers";
46

5-
export type GallerySection =
7+
export type GallerySection =
68
| "Images"
79
| "Audios"
810
| "Videos"
@@ -12,7 +14,41 @@ export type GallerySection =
1214
| "NetworkFiles"
1315
| "Snippets"
1416
| "Miscellaneous"
15-
| "PdfDocuments"
17+
| "PdfDocuments";
18+
19+
export const GALLERY_SECTION = {
20+
IMAGES: "Images",
21+
AUDIOS: "Audios",
22+
VIDEOS: "Videos",
23+
DOCUMENTS: "Documents",
24+
CHEMISTRY: "Chemistry",
25+
DMPS: "DMPs",
26+
NETWORKFILES: "NetworkFiles",
27+
SNIPPETS: "Snippets",
28+
MISCELLANEOUS: "Miscellaneous",
29+
PDFDOCUMENTS: "PdfDocuments",
30+
};
31+
32+
export const parseGallerySectionFromUrlSearchParams = (
33+
searchParams: URLSearchParams
34+
): Result<GallerySection> =>
35+
Result.fromNullable(
36+
searchParams.get("mediaType"),
37+
new Error("No search parameter with name 'mediaType'")
38+
).flatMap((mediaType) =>
39+
Result.first(
40+
Parsers.parseString(GALLERY_SECTION.IMAGES, mediaType),
41+
Parsers.parseString(GALLERY_SECTION.AUDIOS, mediaType),
42+
Parsers.parseString(GALLERY_SECTION.VIDEOS, mediaType),
43+
Parsers.parseString(GALLERY_SECTION.DOCUMENTS, mediaType),
44+
Parsers.parseString(GALLERY_SECTION.CHEMISTRY, mediaType),
45+
Parsers.parseString(GALLERY_SECTION.DMPS, mediaType),
46+
Parsers.parseString(GALLERY_SECTION.NETWORKFILES, mediaType),
47+
Parsers.parseString(GALLERY_SECTION.SNIPPETS, mediaType),
48+
Parsers.parseString(GALLERY_SECTION.MISCELLANEOUS, mediaType),
49+
Parsers.parseString(GALLERY_SECTION.PDFDOCUMENTS, mediaType)
50+
)
51+
);
1652

1753
export const gallerySectionLabel = {
1854
Images: "Images",

src/main/webapp/ui/src/eln/gallery/components/ActionsMenu.js

+20-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import Result from "../../../util/result";
3030
import MoveToIrods, { COLOR as IRODS_COLOR } from "./MoveToIrods";
3131
import IrodsLogo from "./IrodsLogo.svg";
3232
import Avatar from "@mui/material/Avatar";
33+
import Typography from "@mui/material/Typography";
3334
import MoveDialog from "./MoveDialog";
3435

3536
const RenameDialog = ({
@@ -209,8 +210,10 @@ function ActionsMenu({ refreshListing, section }: ActionsMenuArgs): Node {
209210
return (
210211
<>
211212
<Button
212-
variant="outlined"
213+
variant="contained"
214+
color="primary"
213215
size="small"
216+
disabled={selection.isEmpty}
214217
aria-haspopup="menu"
215218
startIcon={<ChecklistIcon />}
216219
onClick={(e) => {
@@ -417,6 +420,22 @@ function ActionsMenu({ refreshListing, section }: ActionsMenuArgs): Node {
417420
disabled={deleteAllowed().isError}
418421
/>
419422
</StyledMenu>
423+
<Typography
424+
variant="body2"
425+
sx={{
426+
p: 0,
427+
pl: 1,
428+
fontWeight: 500,
429+
display: { xs: "none", sm: "initial" },
430+
...(selection.isEmpty
431+
? {
432+
color: "grey",
433+
}
434+
: {}),
435+
}}
436+
>
437+
{selection.label}
438+
</Typography>
420439
</>
421440
);
422441
}

0 commit comments

Comments
 (0)