Make accordion hook (#4464)

# Description of Changes
Make accordion hook, which controls step collapsed state, enforcing only
one step being open at any time
This commit is contained in:
James Brunton 2025-09-18 13:33:54 +01:00 committed by GitHub
parent d2de8e54aa
commit ae7be50ec2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 142 additions and 27 deletions

View File

@ -0,0 +1,123 @@
import { useState, useCallback, useMemo, useEffect } from 'react';
/**
* State conditions that affect accordion behavior
*/
export interface AccordionStateConditions {
/** Whether files are present (steps collapse when false) */
hasFiles?: boolean;
/** Whether results are available (steps collapse when true) */
hasResults?: boolean;
/** Whether the accordion is disabled (steps collapse when true) */
disabled?: boolean;
}
/**
* Configuration for the useAccordionSteps hook
*/
export interface UseAccordionStepsConfig<T extends string | number | symbol> {
/** Special step that represents "no step open" state */
noneValue: T;
/** Initial step to open */
initialStep: T;
/** Current state conditions that affect accordion behavior */
stateConditions?: AccordionStateConditions;
/** Callback to run when interacting with a step when we have results (usually used for resetting params) */
afterResults?: () => void;
}
/**
* Return type for the useAccordionSteps hook
*/
export interface AccordionStepsAPI<T extends string | number | symbol> {
/** Currently active/open step (noneValue if no step is open) */
currentStep: T;
/** Get whether a specific step should be collapsed */
getCollapsedState: (step: T) => boolean;
/** Toggle a step open/closed (accordion behavior - only one open at a time) */
handleStepToggle: (step: T) => void;
/** Set the currently open step */
setOpenStep: (step: T) => void;
/** Close all steps */
closeAllSteps: () => void;
}
/**
* Accordion-style step management hook.
*
* Provides sophisticated accordion behavior where only one step can be open at a time,
* with configurable collapse conditions.
*/
export function useAccordionSteps<T extends string | number | symbol>(
config: UseAccordionStepsConfig<T>
): AccordionStepsAPI<T> {
const { initialStep, stateConditions, noneValue } = config;
const [openStep, setOpenStep] = useState<T>(initialStep);
// Determine if all steps should be collapsed based on conditions
const shouldCollapseAll = useMemo(() => {
if (!stateConditions) {
return false;
}
return (
(stateConditions.hasFiles === false) ||
(stateConditions.hasResults === true) ||
(stateConditions.disabled === true)
);
}, [stateConditions]);
// Get collapsed state for a specific step
const getCollapsedState = useCallback((step: T): boolean => {
if (shouldCollapseAll) {
return true;
} else {
return openStep !== step;
}
}, [openStep, shouldCollapseAll]);
// Handle step toggle with accordion behavior
const handleStepToggle = useCallback((step: T) => {
if (stateConditions?.hasResults) {
config.afterResults?.();
}
// If all steps should be collapsed, don't allow opening
if (shouldCollapseAll) {
return;
}
// Accordion behavior: if clicking the open step, close it; otherwise open the clicked step
setOpenStep(currentStep => {
if (currentStep === step) {
// Clicking the open step - close it
return noneValue;
} else {
// Open the clicked step
return step;
}
});
}, [shouldCollapseAll, noneValue, stateConditions?.hasResults, config.afterResults]);
// Close all steps
const closeAllSteps = useCallback(() => {
setOpenStep(noneValue);
}, [noneValue]);
// Automatically reset to first step if we have results
// Note that everything is still collapsed when this happens, it's just preparing for re-running the tool
useEffect(() => {
if (stateConditions?.hasResults) {
setOpenStep(initialStep);
}
}, [stateConditions?.hasResults, initialStep]);
return {
currentStep: shouldCollapseAll ? noneValue : openStep,
getCollapsedState,
handleStepToggle,
setOpenStep,
closeAllSteps
};
}

View File

@ -1,6 +1,6 @@
import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { createToolFlow } from "../components/tools/shared/createToolFlow"; import { createToolFlow } from "../components/tools/shared/createToolFlow";
import { useAccordionSteps } from "../hooks/tools/shared/useAccordionSteps";
import DeleteAllStep from "../components/tools/changeMetadata/steps/DeleteAllStep"; import DeleteAllStep from "../components/tools/changeMetadata/steps/DeleteAllStep";
import StandardMetadataStep from "../components/tools/changeMetadata/steps/StandardMetadataStep"; import StandardMetadataStep from "../components/tools/changeMetadata/steps/StandardMetadataStep";
import DocumentDatesStep from "../components/tools/changeMetadata/steps/DocumentDatesStep"; import DocumentDatesStep from "../components/tools/changeMetadata/steps/DocumentDatesStep";
@ -34,9 +34,6 @@ const ChangeMetadata = (props: BaseToolProps) => {
const documentDatesTips = useDocumentDatesTips(); const documentDatesTips = useDocumentDatesTips();
const advancedOptionsTips = useAdvancedOptionsTips(); const advancedOptionsTips = useAdvancedOptionsTips();
// Individual step collapse states - only one can be open at a time
const [openStep, setOpenStep] = useState<MetadataStep>(MetadataStep.DELETE_ALL);
const base = useBaseTool( const base = useBaseTool(
'changeMetadata', 'changeMetadata',
useChangeMetadataParameters, useChangeMetadataParameters,
@ -47,27 +44,22 @@ const ChangeMetadata = (props: BaseToolProps) => {
// Extract metadata from uploaded files // Extract metadata from uploaded files
const { isExtractingMetadata } = useMetadataExtraction(base.params); const { isExtractingMetadata } = useMetadataExtraction(base.params);
// Compute actual collapsed state based on results and accordion behavior // Accordion step management
const getActualCollapsedState = (stepName: MetadataStep) => { const accordion = useAccordionSteps<MetadataStep>({
return (!base.hasFiles || base.hasResults) ? true : openStep !== stepName; noneValue: MetadataStep.NONE,
}; initialStep: MetadataStep.DELETE_ALL,
stateConditions: {
// Handle step toggle for accordion behavior hasFiles: base.hasFiles,
const handleStepToggle = (stepName: MetadataStep) => { hasResults: base.hasResults
if (base.hasResults) { },
if (base.settingsCollapsed) { afterResults: base.handleSettingsReset,
base.handleSettingsReset(); });
}
return;
}
setOpenStep(openStep === stepName ? MetadataStep.NONE : stepName);
};
// Create step objects // Create step objects
const createStandardMetadataStep = () => ({ const createStandardMetadataStep = () => ({
title: t("changeMetadata.standardFields.title", "Standard Fields"), title: t("changeMetadata.standardFields.title", "Standard Fields"),
isCollapsed: getActualCollapsedState(MetadataStep.STANDARD_METADATA), isCollapsed: accordion.getCollapsedState(MetadataStep.STANDARD_METADATA),
onCollapsedClick: () => handleStepToggle(MetadataStep.STANDARD_METADATA), onCollapsedClick: () => accordion.handleStepToggle(MetadataStep.STANDARD_METADATA),
tooltip: standardMetadataTips, tooltip: standardMetadataTips,
content: ( content: (
<StandardMetadataStep <StandardMetadataStep
@ -80,8 +72,8 @@ const ChangeMetadata = (props: BaseToolProps) => {
const createDocumentDatesStep = () => ({ const createDocumentDatesStep = () => ({
title: t("changeMetadata.dates.title", "Date Fields"), title: t("changeMetadata.dates.title", "Date Fields"),
isCollapsed: getActualCollapsedState(MetadataStep.DOCUMENT_DATES), isCollapsed: accordion.getCollapsedState(MetadataStep.DOCUMENT_DATES),
onCollapsedClick: () => handleStepToggle(MetadataStep.DOCUMENT_DATES), onCollapsedClick: () => accordion.handleStepToggle(MetadataStep.DOCUMENT_DATES),
tooltip: documentDatesTips, tooltip: documentDatesTips,
content: ( content: (
<DocumentDatesStep <DocumentDatesStep
@ -94,8 +86,8 @@ const ChangeMetadata = (props: BaseToolProps) => {
const createAdvancedOptionsStep = () => ({ const createAdvancedOptionsStep = () => ({
title: t("changeMetadata.advanced.title", "Advanced Options"), title: t("changeMetadata.advanced.title", "Advanced Options"),
isCollapsed: getActualCollapsedState(MetadataStep.ADVANCED_OPTIONS), isCollapsed: accordion.getCollapsedState(MetadataStep.ADVANCED_OPTIONS),
onCollapsedClick: () => handleStepToggle(MetadataStep.ADVANCED_OPTIONS), onCollapsedClick: () => accordion.handleStepToggle(MetadataStep.ADVANCED_OPTIONS),
tooltip: advancedOptionsTips, tooltip: advancedOptionsTips,
content: ( content: (
<AdvancedOptionsStep <AdvancedOptionsStep
@ -114,8 +106,8 @@ const ChangeMetadata = (props: BaseToolProps) => {
const steps = [ const steps = [
{ {
title: t("changeMetadata.deleteAll.label", "Remove Existing Metadata"), title: t("changeMetadata.deleteAll.label", "Remove Existing Metadata"),
isCollapsed: getActualCollapsedState(MetadataStep.DELETE_ALL), isCollapsed: accordion.getCollapsedState(MetadataStep.DELETE_ALL),
onCollapsedClick: () => handleStepToggle(MetadataStep.DELETE_ALL), onCollapsedClick: () => accordion.handleStepToggle(MetadataStep.DELETE_ALL),
tooltip: deleteAllTips, tooltip: deleteAllTips,
content: ( content: (
<DeleteAllStep <DeleteAllStep