Skip to content

Commit

Permalink
Added the ability to move question (#274)
Browse files Browse the repository at this point in the history
* Part 1 of moving question to another page

* Part 2 of moving question to another page

* Added the ability to move question to another page

* Added the ability to move question to another page

* fixing get by role selector to remove ambiguity

* removed logging

* Removed some console logs

* Fixed some bugs and accessibility issues

This commit includes a fix for the bug when moving questions back and forth and also a bug with the list of page options. This update also addresses some accessibility issues.

* Fixed some bugs and accessibility issues part 2

* Addressed some bugs and feedback reviews

In this commit I fixed a couple of bugs and made updates per some reviews in the pull request.

* Addressed some bugs and feedback reviews Part 2

* Updated the builder test to account for source page

* Made updates per the reviews in my PR

* Made updates per the reviews in my PR Part 2

* Ensured that page types can't be moved

* A minor update to the store.ts

* A minor update to the store.ts Part 2

---------

Co-authored-by: ethangardner <[email protected]>
  • Loading branch information
natashapl and ethangardner authored Aug 20, 2024
1 parent ede6349 commit 7e0fd83
Show file tree
Hide file tree
Showing 10 changed files with 754 additions and 44 deletions.
1 change: 1 addition & 0 deletions apps/spotlight/src/env.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />
2 changes: 1 addition & 1 deletion e2e/src/create.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const createNewForm = async (page: Page) => {
}

const addQuestions = async (page: Page) => {
const menuButton = page.getByRole('button', { name: 'Question' });
const menuButton = page.getByRole('button', { name: 'Question', exact: true });
await menuButton.click();
await page.getByRole('button', { name: 'Short Answer' }).click();
await menuButton.click();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,12 +282,9 @@ const AddPatternDropdownContent = ({
className={`${styles.dropdownMenu} usa-list usa-list--unstyled position-absolute bg-white z-100 shadow-3 text-left`}
>
{availablePatterns.map(([patternType, pattern], index) => (
<li
key={index}
className={`${styles.dropdownItem} padding-1 cursor-pointer margin-left-1`}
>
<li key={index} className={`${styles.dropdownItem} margin-left-1`}>
<button
className="bg-transparent padding-0 border-0"
className="bg-transparent padding-1 text-left width-full cursor-pointer border-0"
onClick={() => {
patternSelected(patternType);
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import React, { useState, useRef, useEffect } from 'react';
import { useFormManagerStore } from '../../../store';
import styles from '../../formEditStyles.module.css';

interface MovePatternDropdownProps {
isFieldset: boolean;
}

// Define the extended type for pages
interface PageWithLabel {
id: string;
type: string;
data: {
title: string;
patterns: string[];
};
specialLabel?: string;
}

const MovePatternDropdown: React.FC<MovePatternDropdownProps> = ({
isFieldset,
}) => {
const context = useFormManagerStore(state => state.context);
const [dropdownOpen, setDropdownOpen] = useState(false);
const [targetPage, setTargetPage] = useState('');
const [moveToPosition, setMoveToPosition] = useState('');
const dropdownRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const pages = useFormManagerStore(state =>
Object.values(state.session.form.patterns).filter(p => p.type === 'page')
);
const movePatternToPage = useFormManagerStore(state => state.movePattern);
const focusPatternId = useFormManagerStore(state => state.focus?.pattern.id);
const useAvailablePages = () => {
const currentPageIndex = pages.findIndex(page =>
page.data.patterns.includes(focusPatternId || '')
);
const page1Count = pages.reduce(
(count, page) => count + (page.data.title === 'Page 1' ? 1 : 0),
0
);
const availablePages: PageWithLabel[] =
page1Count > 1
? pages.slice(1).map((page, index) => {
if (index + 1 === currentPageIndex) {
return { ...page, specialLabel: 'Current page' };
}
return page;
})
: pages.map((page, index) => {
if (index === currentPageIndex) {
return { ...page, specialLabel: 'Current page' };
}
return page;
});

return availablePages;
};
const availablePages = useAvailablePages();
const currentPageIndex = pages.findIndex(page =>
page.data.patterns.includes(focusPatternId || '')
);
const sourcePage = pages[currentPageIndex]?.id;
const handleMovePattern = () => {
if (focusPatternId && targetPage) {
movePatternToPage(sourcePage, targetPage, focusPatternId, moveToPosition);
}
setDropdownOpen(false);
};
const toggleDropdown = () => {
setDropdownOpen(!dropdownOpen);
};
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setDropdownOpen(false);
}
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setDropdownOpen(false);
buttonRef.current?.focus();
}
};

useEffect(() => {
if (dropdownOpen) {
document.addEventListener('mousedown', handleClickOutside);
dropdownRef.current?.addEventListener('keydown', handleKeyDown);
} else {
document.removeEventListener('mousedown', handleClickOutside);
dropdownRef.current?.removeEventListener('keydown', handleKeyDown);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
dropdownRef.current?.removeEventListener('keydown', handleKeyDown);
};
}, [dropdownOpen]);

return (
<div
className={`${styles.moveToPageWrapper} display-inline-block text-ttop position-relative`}
ref={dropdownRef}
>
<p
className={`${styles.movePatternButton} margin-top-1 display-inline-block text-ttop cursor-pointer`}
>
<button
className="usa-button--outline usa-button--unstyled margin-right-0 padding-top-1 padding-left-05 padding-bottom-05"
type="button"
ref={buttonRef}
aria-haspopup="true"
aria-expanded={dropdownOpen ? 'true' : 'false'}
onClick={event => {
event.preventDefault();
toggleDropdown();
}}
>
<span className="display-inline-block text-ttop">
{isFieldset ? 'Move fieldset' : 'Move question'}
</span>
<svg
className="usa-icon display-inline-block text-ttop"
aria-hidden="true"
focusable="false"
role="img"
>
<use
xlinkHref={`${context.uswdsRoot}img/sprite.svg#expand_more`}
></use>
</svg>
</button>
</p>
{dropdownOpen && (
<div className={`${styles.dropDown} padding-2`} tabIndex={-1}>
<div className={`${styles.moveToPagePosition} margin-bottom-1`}>
<label
className="usa-label display-inline-block text-ttop margin-right-1"
htmlFor="pagenumbers"
>
Page:
</label>
<select
className="usa-select display-inline-block text-ttop"
name="pagenumbers"
id="pagenumbers"
value={targetPage}
onChange={e => setTargetPage(e.target.value)}
>
<option value="" disabled>
Select page
</option>
{availablePages.map((page, index) => (
<option key={page.id} value={page.id}>
{page.specialLabel || page.data.title || `Page ${index + 2}`}
</option>
))}
</select>
</div>
<div className={`${styles.moveToPagePosition} margin-bottom-1`}>
<label
className="usa-label margin-right-1 display-inline-block text-ttop"
htmlFor="elementPosition"
>
Position:
</label>
<select
className="usa-select display-inline-block text-ttop"
name="elementPosition"
id="elementPosition"
value={moveToPosition}
onChange={e =>
setMoveToPosition(e.target.value as 'top' | 'bottom')
}
>
<option value="" disabled>
Select position
</option>
<option value="top">Top of page</option>
<option value="bottom">Bottom of page</option>
</select>
</div>
<p>
<button
type="button"
aria-label={
isFieldset
? 'Move fieldset to another page'
: 'Move question to another page'
}
title={
isFieldset
? 'Move fieldset to another page'
: 'Move question to another page'
}
className="usa-button margin-right-0"
onClick={handleMovePattern}
>
{isFieldset ? 'Move fieldset' : 'Move question'}
</button>
</p>
</div>
)}
</div>
);
};

export default MovePatternDropdown;
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React, { PropsWithChildren, ReactElement } from 'react';
import React, { PropsWithChildren, ReactElement, useMemo } from 'react';
import classNames from 'classnames';

import { useFormManagerStore } from '../../../store';
import MovePatternDropdown from './MovePatternDropdown';
import styles from '../../formEditStyles.module.css';

type PatternEditActionsProps = PropsWithChildren<{
children?: ReactElement;
Expand All @@ -13,16 +15,42 @@ export const PatternEditActions = ({ children }: PatternEditActionsProps) => {
const { deleteSelectedPattern } = useFormManagerStore(state => ({
deleteSelectedPattern: state.deleteSelectedPattern,
}));
const focusPatternType = useFormManagerStore(
state => state.focus?.pattern.type
);
const patterns = useFormManagerStore(state =>
Object.values(state.session.form.patterns)
);
const focusPatternId = useFormManagerStore(state => state.focus?.pattern.id);
const isPatternInFieldset = useMemo(() => {
if (!focusPatternId) return false;
return patterns.some(
p => p.type === 'fieldset' && p.data.patterns.includes(focusPatternId)
);
}, [focusPatternId, patterns]);

const isFieldset = focusPatternType === 'fieldset';
const isPagePattern = focusPatternType === 'page';

return (
<>
<div className="border-top-1px border-base-lighter margin-top-2 margin-bottom-2 padding-top-1 width-full text-right pattern-edit-panel base-dark">
<span
className={classNames('display-inline-block', {
<div
className={`${styles.patternActionWrapper} margin-top-2 margin-bottom-1 padding-top-1 width-full pattern-edit-panel base-dark text-right`}
>
<div
className={classNames(
'border-top-1px border-bottom-1px border-base-lighter ',
{
'border-base-lighter': children,
'padding-right-1': children,
'margin-right-1': children,
})}
}
)}
>
{!isPatternInFieldset && !isPagePattern && (
<MovePatternDropdown isFieldset={isFieldset} />
)}
<span
className={`${styles.patternActionButtons} margin-top-1 margin-bottom-1 display-inline-block text-ttop`}
>
<button
type="button"
Expand Down Expand Up @@ -53,7 +81,12 @@ export const PatternEditActions = ({ children }: PatternEditActionsProps) => {
className="usa-button--outline usa-button--unstyled"
onClick={event => {
event.preventDefault();
deleteSelectedPattern();
const confirmed = window.confirm(
'Are you sure you want to delete this question?'
);
if (confirmed) {
deleteSelectedPattern();
}
}}
>
<svg
Expand All @@ -67,30 +100,21 @@ export const PatternEditActions = ({ children }: PatternEditActionsProps) => {
></use>
</svg>
</button>

<button
type="submit"
aria-label="Save changes to this pattern"
title="Save changes to this pattern"
className="usa-button--outline usa-button--unstyled text-success hover:text-success"
>
<svg
className="usa-icon usa-icon--size-3 margin-1 text-middle"
aria-hidden="true"
focusable="false"
role="img"
>
<use xlinkHref={`${context.uswdsRoot}img/sprite.svg#check`}></use>
</svg>
</button>

{children ? (
<span className="margin-left-1 padding-left-2 border-left-1px border-base-lighter">
{children}
</span>
<span className="padding-left-1 padding-top-2px">{children}</span>
) : null}
</span>
</div>
</>
<div className="padding-top-2">
<button
type="submit"
aria-label="Save and Close"
title="Save and Close"
className="usa-button"
>
Save and Close
</button>
</div>
</div>
);
};
Loading

0 comments on commit 7e0fd83

Please sign in to comment.