Skip to content

Commit bfb468e

Browse files
CopilotDevSecNinja
andauthored
feat: two-step X to hide the measure detail panel, reopening on measure click (#95)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Jean-Paul van Ravensberg <14926452+DevSecNinja@users.noreply.github.com>
1 parent 893f5df commit bfb468e

5 files changed

Lines changed: 80 additions & 13 deletions

File tree

‎src/App.jsx‎

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export default function App() {
6262
const [typeFilter, setTypeFilter] = useState(null);
6363
const [tierFilter, setTierFilter] = useState(null);
6464
const [selectedCode, setSelectedCode] = useState(null);
65+
const [showMeasureCard, setShowMeasureCard] = useState(true);
6566
const [assessmentMode, setAssessmentMode] = useState(false);
6667

6768
const {
@@ -97,6 +98,22 @@ export default function App() {
9798
const ActiveComponent = activeView.Component;
9899
const filterable = activeView.filterable;
99100

101+
// Selecting a measure (re)opens the detail card if it was hidden.
102+
const handleSelect = (code) => {
103+
setShowMeasureCard(true);
104+
setSelectedCode(code);
105+
};
106+
107+
// A single X control handles two steps: first deselect the measure, then
108+
// (when nothing is selected) hide the whole card.
109+
const handleCloseOrHide = () => {
110+
if (selected) {
111+
setSelectedCode(null);
112+
} else {
113+
setShowMeasureCard(false);
114+
}
115+
};
116+
100117
return (
101118
<div className="app">
102119
<AppHeader
@@ -121,28 +138,30 @@ export default function App() {
121138
/>
122139
)}
123140

124-
<div className="app__body">
141+
<div className={`app__body${showMeasureCard ? '' : ' app__body--full'}`}>
125142
<main className="app__main">
126143
<ActiveComponent
127144
lang={lang}
128145
selectedCode={selectedCode}
129-
onSelect={setSelectedCode}
146+
onSelect={handleSelect}
130147
typeFilter={filterable ? typeFilter : null}
131148
tierFilter={filterable ? tierFilter : null}
132149
getAssessmentStatus={assessmentMode ? getStatus : null}
133150
/>
134151
</main>
135152

136-
<MeasureDetailPanel
137-
measure={selected}
138-
lang={lang}
139-
onClose={() => setSelectedCode(null)}
140-
assessmentEntry={
141-
assessmentMode && selected ? getEntry(selected.code) : undefined
142-
}
143-
onStatusChange={assessmentMode ? setStatus : undefined}
144-
onNoteChange={assessmentMode ? setNote : undefined}
145-
/>
153+
{showMeasureCard && (
154+
<MeasureDetailPanel
155+
measure={selected}
156+
lang={lang}
157+
onClose={handleCloseOrHide}
158+
assessmentEntry={
159+
assessmentMode && selected ? getEntry(selected.code) : undefined
160+
}
161+
onStatusChange={assessmentMode ? setStatus : undefined}
162+
onNoteChange={assessmentMode ? setNote : undefined}
163+
/>
164+
)}
146165
</div>
147166

148167
<footer className="app__footer">

‎src/App.test.jsx‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,33 @@ describe('App', () => {
113113
expect(screen.getByText('Middellange termijn')).toBeInTheDocument();
114114
expect(screen.getByText('Lange termijn')).toBeInTheDocument();
115115
});
116+
117+
it('hides the measure card with a two-step X and reopens it on measure click', async () => {
118+
const user = userEvent.setup();
119+
render(<App />);
120+
121+
// Select a measure: the detail panel shows it.
122+
await user.click(screen.getByRole('button', { name: /O7/ }));
123+
let panel = screen.getByRole('complementary');
124+
expect(
125+
within(panel).getByText('Microsoft Entra Conditional Access')
126+
).toBeInTheDocument();
127+
128+
// First X click deselects the measure but keeps the card open.
129+
await user.click(within(panel).getByRole('button', { name: 'Sluiten' }));
130+
panel = screen.getByRole('complementary');
131+
expect(within(panel).getByText('Selecteer een maatregel')).toBeInTheDocument();
132+
133+
// Second X click (nothing selected) hides the whole card.
134+
await user.click(within(panel).getByRole('button', { name: 'Paneel verbergen' }));
135+
expect(screen.queryByRole('complementary')).not.toBeInTheDocument();
136+
expect(document.querySelector('.app__body')).toHaveClass('app__body--full');
137+
138+
// Clicking a measure reopens the card and shows the selection.
139+
await user.click(screen.getByRole('button', { name: /O7/ }));
140+
panel = screen.getByRole('complementary');
141+
expect(
142+
within(panel).getByText('Microsoft Entra Conditional Access')
143+
).toBeInTheDocument();
144+
});
116145
});

‎src/components/MeasureDetailPanel.jsx‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ export default function MeasureDetailPanel({
3636
if (!measure) {
3737
return (
3838
<aside ref={panelRef} className="detail detail--empty" aria-live="polite">
39+
<button
40+
type="button"
41+
className="detail__close detail__close--floating"
42+
onClick={onClose}
43+
aria-label={t(lang, 'hideMeasures')}
44+
>
45+
×
46+
</button>
3947
<h2 className="detail__empty-title">{t(lang, 'selectMeasure')}</h2>
4048
<p className="detail__empty-hint">{t(lang, 'selectMeasureHint')}</p>
4149
</aside>

‎src/i18n/strings.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export const ui = {
7070
'Indicatieve koppeling met veelgebruikte controlekaders; geen 1-op-1-relatie en niet uitputtend.',
7171
openDocs: 'Documentatie openen',
7272
close: 'Sluiten',
73+
hideMeasures: 'Paneel verbergen',
7374
selectMeasure: 'Selecteer een maatregel',
7475
selectMeasureHint:
7576
'Kies een GRIP-maatregel om de bijpassende Microsoft-oplossingen te zien.',
@@ -181,6 +182,7 @@ export const ui = {
181182
'Indicative mapping to common control frameworks; not a 1:1 relationship and not exhaustive.',
182183
openDocs: 'Open documentation',
183184
close: 'Close',
185+
hideMeasures: 'Hide panel',
184186
selectMeasure: 'Select a measure',
185187
selectMeasureHint: 'Pick a GRIP measure to see the matching Microsoft solutions.',
186188
filters: 'Filters',
@@ -293,6 +295,7 @@ export const ui = {
293295
'Correspondance indicative avec des cadres de contrôle courants ; relation non biunivoque et non exhaustive.',
294296
openDocs: 'Ouvrir la documentation',
295297
close: 'Fermer',
298+
hideMeasures: 'Masquer le panneau',
296299
selectMeasure: 'Sélectionnez une mesure',
297300
selectMeasureHint:
298301
'Choisissez une mesure GRIP pour voir les solutions Microsoft correspondantes.',

‎src/styles/index.css‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,9 @@ body {
223223
align-items: start;
224224
flex: 1;
225225
}
226+
.app__body--full {
227+
grid-template-columns: 1fr;
228+
}
226229
.app__main {
227230
min-width: 0;
228231
}
@@ -488,12 +491,17 @@ body {
488491
.detail--empty {
489492
display: flex;
490493
flex-direction: column;
491-
justify-content: center;
494+
justify-content: flex-start;
492495
text-align: center;
493496
color: var(--ink-faint);
494497
border-style: dashed;
495498
box-shadow: none;
496499
}
500+
.detail__close--floating {
501+
position: absolute;
502+
top: 18px;
503+
right: 18px;
504+
}
497505
.detail__empty-title {
498506
font-size: 16px;
499507
margin: 0 0 6px;

0 commit comments

Comments
 (0)