Skip to content

Commit bf7d329

Browse files
Update manual images (#193)
* Update content and introduce in-page back links * Add better display row, fix bugs, add count * Add screening room selection page and data
1 parent c010226 commit bf7d329

File tree

18 files changed

+1263
-302
lines changed

18 files changed

+1263
-302
lines changed
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// app/assets/javascript/stepper-input.js
2+
3+
// Initialize stepper input (number input with increase/decrease buttons)
4+
document.addEventListener('DOMContentLoaded', () => {
5+
// Prevent text selection when rapidly clicking buttons
6+
// This stops the browser from selecting page text when users click the +/- buttons quickly
7+
// We still allow selection on the input itself so users can select/copy the value
8+
const wrappers = document.querySelectorAll('.app-stepper-input__wrapper')
9+
wrappers.forEach((wrapper) => {
10+
wrapper.addEventListener('selectstart', (e) => {
11+
// Only prevent selection if it's not the input field being selected
12+
if (e.target.tagName !== 'INPUT') {
13+
e.preventDefault()
14+
}
15+
})
16+
})
17+
18+
// Update button disabled states based on current value
19+
const updateButtonStates = (input) => {
20+
const minAttr = input.getAttribute('min')
21+
const maxAttr = input.getAttribute('max')
22+
const min = minAttr !== null ? parseInt(minAttr, 10) : null
23+
const max = maxAttr !== null ? parseInt(maxAttr, 10) : null
24+
const value = parseInt(input.value, 10) || 0
25+
26+
const inputId = input.getAttribute('id')
27+
const decreaseButton = document.querySelector(
28+
`.app-stepper-input__button--decrease[data-input-id="${inputId}"]`
29+
)
30+
const increaseButton = document.querySelector(
31+
`.app-stepper-input__button--increase[data-input-id="${inputId}"]`
32+
)
33+
34+
if (decreaseButton) {
35+
decreaseButton.disabled = min !== null && value <= min
36+
}
37+
38+
if (increaseButton) {
39+
increaseButton.disabled = max !== null && value >= max
40+
}
41+
}
42+
43+
// Announce value changes to screen readers
44+
const announceValue = (inputId, value, atLimit = false) => {
45+
const statusElement = document.getElementById(`${inputId}-status`)
46+
if (statusElement) {
47+
let announcement
48+
49+
// Handle negative numbers explicitly for screen readers
50+
if (typeof value === 'number' && value < 0) {
51+
announcement = `negative ${Math.abs(value)}`
52+
} else {
53+
announcement = String(value)
54+
}
55+
56+
// Add limit reached message if at boundary
57+
if (atLimit) {
58+
announcement += ', limit reached'
59+
}
60+
61+
statusElement.textContent = announcement
62+
}
63+
}
64+
65+
const buttons = document.querySelectorAll('.app-stepper-input__button')
66+
67+
buttons.forEach((button) => {
68+
// Prevent text selection on rapid clicks
69+
button.addEventListener('selectstart', (e) => {
70+
e.preventDefault()
71+
})
72+
73+
button.addEventListener('click', () => {
74+
const inputId = button.getAttribute('data-input-id')
75+
const input = document.getElementById(inputId)
76+
77+
if (!input) {
78+
return
79+
}
80+
81+
const currentValue = parseInt(input.value, 10) || 0
82+
const minAttr = input.getAttribute('min')
83+
const maxAttr = input.getAttribute('max')
84+
const min = minAttr !== null ? parseInt(minAttr, 10) : null
85+
const max = maxAttr !== null ? parseInt(maxAttr, 10) : null
86+
const step = parseInt(input.getAttribute('step'), 10) || 1
87+
88+
let newValue = currentValue
89+
90+
if (button.classList.contains('app-stepper-input__button--increase')) {
91+
newValue = currentValue + step
92+
if (max !== null) {
93+
newValue = Math.min(newValue, max)
94+
}
95+
} else if (
96+
button.classList.contains('app-stepper-input__button--decrease')
97+
) {
98+
newValue = currentValue - step
99+
if (min !== null) {
100+
newValue = Math.max(newValue, min)
101+
}
102+
}
103+
104+
input.value = newValue
105+
106+
// Update button states
107+
updateButtonStates(input)
108+
109+
// Announce new value, and add "limit reached" if we've hit a boundary
110+
const reachedMax = max !== null && newValue === max
111+
const reachedMin = min !== null && newValue === min
112+
113+
announceValue(inputId, newValue, reachedMax || reachedMin)
114+
115+
// Trigger input and change events so other listeners can respond
116+
input.dispatchEvent(new Event('input', { bubbles: true }))
117+
input.dispatchEvent(new Event('change', { bubbles: true }))
118+
})
119+
})
120+
121+
// Handle manual input to enforce min/max
122+
const inputs = document.querySelectorAll(
123+
'.app-stepper-input input[type="text"][inputmode="numeric"]'
124+
)
125+
126+
inputs.forEach((input) => {
127+
input.addEventListener('blur', () => {
128+
const minAttr = input.getAttribute('min')
129+
const maxAttr = input.getAttribute('max')
130+
const min = minAttr !== null ? parseInt(minAttr, 10) : null
131+
const max = maxAttr !== null ? parseInt(maxAttr, 10) : null
132+
let value = parseInt(input.value, 10)
133+
134+
if (isNaN(value)) {
135+
value = min !== null ? min : 0
136+
} else {
137+
if (min !== null && value < min) {
138+
value = min
139+
}
140+
if (max !== null && value > max) {
141+
value = max
142+
}
143+
}
144+
145+
input.value = value
146+
147+
// Update button states after manual input
148+
updateButtonStates(input)
149+
})
150+
151+
// Also update on input event for immediate feedback
152+
input.addEventListener('input', () => {
153+
updateButtonStates(input)
154+
})
155+
})
156+
})

app/assets/sass/_misc.scss

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@
1515
}
1616
}
1717

18+
// Progressive enhancement helpers
19+
.app-js-only {
20+
display: none;
21+
}
22+
23+
body.js-enabled .app-js-only {
24+
display: block;
25+
}
26+
27+
body.js-enabled .app-no-js-only {
28+
display: none;
29+
}
30+
1831
.app-image-two-up {
1932
display: flex;
2033
justify-content: space-between;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// app/assets/sass/components/_stepper-input.scss
2+
3+
.app-stepper-input {
4+
user-select: none;
5+
}
6+
7+
.app-stepper-input__wrapper {
8+
display: flex;
9+
align-items: stretch;
10+
gap: 8px;
11+
flex-wrap: nowrap;
12+
}
13+
14+
.app-stepper-input__button {
15+
margin-bottom: 4px !important;
16+
flex-shrink: 0;
17+
align-self: stretch;
18+
display: flex;
19+
align-items: center;
20+
justify-content: center;
21+
width: auto !important;
22+
min-width: 0 !important;
23+
user-select: none;
24+
25+
// Ensure focus state is visible above input
26+
&:focus {
27+
position: relative;
28+
z-index: 1;
29+
}
30+
31+
&:disabled,
32+
&[disabled] {
33+
cursor: not-allowed !important;
34+
}
35+
36+
// Icon sizing
37+
svg {
38+
display: block;
39+
pointer-events: none;
40+
user-select: none;
41+
}
42+
}
43+
44+
.app-stepper-input input.nhsuk-input {
45+
text-align: center;
46+
}

app/assets/sass/main.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ $nhsuk-page-width: 1260px;
2727
@forward "components/sticky-appointment-bar";
2828
@forward "components/small-button";
2929
@forward "components/annotation";
30+
@forward "components/stepper-input";
3031

3132
@forward "misc";
3233
@forward "workflow";

app/data/screening-rooms.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// app/data/screening-rooms.js
2+
3+
module.exports = [
4+
{
5+
id: 'room-a-hologic',
6+
displayName: 'Room A - Hologic XYZ123',
7+
locationId: 'duif1ywp' // West of London BSS hospital
8+
},
9+
{
10+
id: 'room-b-ge',
11+
displayName: 'Room B - GE ABC456',
12+
locationId: 'duif1ywp'
13+
},
14+
{
15+
id: 'room-c-siemens',
16+
displayName: 'Room C - Siemens DEF789',
17+
locationId: 'duif1ywp'
18+
},
19+
{
20+
id: 'room-d-ge',
21+
displayName: 'Room D - GE GHI012',
22+
locationId: 'duif1ywp'
23+
}
24+
]

app/data/session-data-defaults.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const breastScreeningUnits = require('./breast-screening-units')
66
// All breast screening units
77
const allBreastScreeningUnits = require('./all-breast-screening-units')
88
const medicalHistoryTypes = require('./medical-history-types')
9+
const screeningRooms = require('./screening-rooms')
910
const path = require('path')
1011
const fs = require('fs')
1112
const { needsRegeneration } = require('../lib/utils/regenerate-data')
@@ -78,8 +79,10 @@ module.exports = {
7879
users,
7980
currentUserId: users[0].id,
8081
currentUser: users[0],
82+
currentScreeningRoom: screeningRooms[0].id,
8183
breastScreeningUnits,
8284
allBreastScreeningUnits,
85+
screeningRooms,
8386
participants,
8487
clinics,
8588
events,

app/lib/generators/mammogram-generator.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,11 @@ const generateViewImages = ({
8787
viewShort: view === 'mediolateral oblique' ? 'MLO' : 'CC',
8888
viewShortWithSide: `${side === 'right' ? 'R' : 'L'}${view === 'mediolateral oblique' ? 'MLO' : 'CC'}`,
8989
images,
90-
isRepeat: needsRepeat && isSeedData,
91-
repeatReason:
90+
count: images.length,
91+
repeatCount: needsRepeat ? 1 : 0,
92+
repeatReasons:
9293
needsRepeat && isSeedData
93-
? faker.helpers.arrayElement(REPEAT_REASONS)
94+
? [faker.helpers.arrayElement(REPEAT_REASONS)]
9495
: null
9596
}
9697
}
@@ -188,7 +189,7 @@ const generateMammogramImages = ({
188189

189190
// Check if any views are missing
190191
const hasMissingViews = Object.keys(views).length < 4
191-
const hasRepeat = Object.values(views).some((view) => view.isRepeat)
192+
const hasRepeat = Object.values(views).some((view) => view.repeatCount > 0)
192193

193194
return {
194195
accessionBase,

0 commit comments

Comments
 (0)