Skip to content

Commit 81cf6e6

Browse files
fix(validation): improve certificate chain visual and accessibility
- Replace NcListItem with plain divs for multi-line cert details - Use dl/dt/dd semantic structure for screen readers - Add aria-expanded, aria-label on toggle button and cert items - Fix layout spacing and reset browser defaults on dt/dd - Add TRANSLATORS comments with PKI context for non-technical translators - Fix CSS typo: phain-wrapper -> padding-left Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent 8a98ba9 commit 81cf6e6

1 file changed

Lines changed: 86 additions & 43 deletions

File tree

src/components/validation/CertificateChain.vue

Lines changed: 86 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
88
class="extra"
99
compact
1010
role="button"
11+
:aria-expanded="chainOpen ? 'true' : 'false'"
1112
@click="chainOpen = !chainOpen">
1213
<template #name>
14+
<!-- TRANSLATORS: "Certificate chain" is the sequence of digital identity cards that prove who signed the document — starting with the signer's own certificate and going up to the authority that vouches for everyone (the root CA). Like a chain of trust: "I trust you because this institution trusts you." -->
1315
<strong>{{ t('libresign', 'Certificate chain') }}</strong>
1416
</template>
1517
<template #extra-actions>
1618
<NcButton variant="tertiary"
19+
:aria-label="getToggleAriaLabel()"
1720
@click.stop="chainOpen = !chainOpen">
1821
<template #icon>
1922
<NcIconSvgWrapper v-if="chainOpen"
@@ -25,41 +28,44 @@
2528
</template>
2629
</NcListItem>
2730

31+
<!-- TRANSLATORS: Label read aloud by screen readers (for blind users) when they navigate into this section. It is not visible on screen. Describes the area that shows the certificate chain (the sequence of digital identity cards behind the signature) -->
2832
<div v-if="chainOpen" class="chain-wrapper" role="region" :aria-label="t('libresign', 'Certificate chain details')">
29-
<NcListItem v-for="(cert, certIndex) in chain"
33+
<div v-for="(cert, certIndex) in chain"
3034
:key="certIndex"
3135
class="extra-chain certificate-item"
32-
compact
33-
:name="certIndex === 0 ? t('libresign', 'Signer:') : t('libresign', 'Issuer:')">
34-
<template #name>
35-
<div class="cert-details">
36-
<div>
37-
<strong>{{ certIndex === 0 ? t('libresign', 'Signer:') : t('libresign', 'Issuer:') }}</strong>
38-
{{ cert.subject?.CN || cert.name || cert.displayName }}
39-
</div>
40-
<div v-if="cert.issuer?.CN" class="cert-issuer">
41-
<strong>{{ t('libresign', 'Issued by:') }}</strong>
42-
{{ cert.issuer.CN }}
43-
</div>
44-
<div v-if="cert.serialNumber">
45-
<strong>{{ t('libresign', 'Serial Number:') }}</strong>
36+
:aria-label="getCertItemLabel(certIndex)">
37+
<dl class="cert-details">
38+
<div class="cert-field">
39+
<dt>{{ getCertRoleLabel(certIndex) }}</dt>
40+
<dd>{{ cert.subject?.CN || cert.name || cert.displayName }}</dd>
41+
</div>
42+
<div v-if="cert.issuer?.CN" class="cert-field cert-issuer">
43+
<!-- TRANSLATORS: Label shown next to the name of the organization or authority (Certificate Authority, CA) that issued and digitally signed the certificate, vouching for authenticity. Like the government agency that issues a passport. -->
44+
<dt>{{ t('libresign', 'Issued by:') }}</dt>
45+
<dd>{{ cert.issuer.CN }}</dd>
46+
</div>
47+
<div v-if="cert.serialNumber" class="cert-field">
48+
<!-- TRANSLATORS: Label shown next to the unique number assigned to a certificate by the issuing authority (CA), used to identify and, if necessary, revoke it. Like a passport number — every certificate has a different one. -->
49+
<dt>{{ t('libresign', 'Serial Number:') }}</dt>
50+
<dd>
4651
{{ cert.serialNumber }}
4752
<span v-if="cert.serialNumberHex" class="serial-hex">
4853
(hex: {{ cert.serialNumberHex }})
4954
</span>
50-
</div>
51-
<div v-if="cert.validFrom_time_t || cert.validTo_time_t">
52-
<small>
53-
<strong v-if="cert.validFrom_time_t">{{ t('libresign', 'Valid from:') }}</strong>
54-
{{ formatTimestamp(cert.validFrom_time_t) }}
55-
<br v-if="cert.validFrom_time_t && cert.validTo_time_t">
56-
<strong v-if="cert.validTo_time_t">{{ t('libresign', 'Valid to:') }}</strong>
57-
{{ formatTimestamp(cert.validTo_time_t) }}
58-
</small>
59-
</div>
55+
</dd>
56+
</div>
57+
<div v-if="cert.validFrom_time_t" class="cert-field">
58+
<!-- TRANSLATORS: Label shown next to the date and time from which a certificate becomes valid. Before that date the certificate cannot be trusted, even if it looks genuine. -->
59+
<dt>{{ t('libresign', 'Valid from:') }}</dt>
60+
<dd>{{ formatTimestamp(cert.validFrom_time_t) }}</dd>
6061
</div>
61-
</template>
62-
</NcListItem>
62+
<div v-if="cert.validTo_time_t" class="cert-field">
63+
<!-- TRANSLATORS: The date and time after which this certificate expires and can no longer be trusted. Like an expiry date on an ID card. -->
64+
<dt>{{ t('libresign', 'Valid to:') }}</dt>
65+
<dd>{{ formatTimestamp(cert.validTo_time_t) }}</dd>
66+
</div>
67+
</dl>
68+
</div>
6369
</div>
6470
</div>
6571
</template>
@@ -106,6 +112,30 @@ export default {
106112
if (!timestamp) return ''
107113
return Moment.unix(timestamp).format('LLL')
108114
},
115+
getToggleAriaLabel() {
116+
if (this.chainOpen) {
117+
// TRANSLATORS: Button label read by screen readers. Clicking it hides the list of certificates in the trust chain (the digital identity cards behind the signature)
118+
return t('libresign', 'Collapse certificate chain')
119+
}
120+
// TRANSLATORS: Button label read by screen readers. Clicking it reveals the list of certificates in the trust chain (the digital identity cards behind the signature)
121+
return t('libresign', 'Expand certificate chain')
122+
},
123+
getCertItemLabel(certIndex) {
124+
if (certIndex === 0) {
125+
// TRANSLATORS: Label read by screen readers to identify the first certificate — the one belonging to the person who actually signed the document
126+
return t('libresign', 'Signer certificate')
127+
}
128+
// TRANSLATORS: Label read by screen readers to identify additional certificates higher up in the trust chain. {index} is a number starting at 2 (e.g. "Certificate 2" is the issuing authority of the signer, "Certificate 3" is the authority above that, and so on)
129+
return t('libresign', 'Certificate {index}', { index: certIndex + 1 })
130+
},
131+
getCertRoleLabel(certIndex) {
132+
if (certIndex === 0) {
133+
// TRANSLATORS: Label shown next to the name of the person or entity who signed the document. Their identity is proven by their certificate.
134+
return t('libresign', 'Signer:')
135+
}
136+
// TRANSLATORS: Label shown next to the name of the Certificate Authority (CA) that issued the certificate above it in the chain. A CA is an organization trusted to verify and certify digital identities, like a notary or government agency.
137+
return t('libresign', 'Issuer:')
138+
},
109139
},
110140
}
111141
</script>
@@ -130,21 +160,13 @@ export default {
130160
}
131161
132162
.extra-chain {
133-
:deep(.list-item-content__name) {
134-
white-space: unset;
135-
display: flex;
136-
align-items: center;
137-
gap: 8px;
138-
}
139-
:deep(.list-item__anchor) {
140-
height: unset;
141-
}
163+
padding: 0;
142164
}
143165
144166
.certificate-item {
145167
border-bottom: 1px solid var(--color-border);
146-
padding-bottom: 12px;
147-
margin-bottom: 12px;
168+
padding-bottom: 4px;
169+
margin-bottom: 4px;
148170
149171
&:last-child {
150172
border-bottom: none;
@@ -156,12 +178,33 @@ export default {
156178
.cert-details {
157179
display: flex;
158180
flex-direction: column;
159-
gap: 8px;
181+
gap: 2px;
160182
width: 100%;
183+
margin: 0;
184+
padding: 4px 0;
185+
list-style: none;
186+
}
161187
162-
> div {
163-
line-height: 1.4;
164-
word-break: break-word;
188+
.cert-field {
189+
display: flex;
190+
flex-wrap: wrap;
191+
align-items: baseline;
192+
gap: 4px;
193+
line-height: 1.5;
194+
word-break: break-word;
195+
196+
dt {
197+
font-weight: bold;
198+
min-width: 90px;
199+
text-align: right;
200+
margin: 0;
201+
padding: 0;
202+
}
203+
204+
dd {
205+
margin: 0;
206+
padding: 0;
207+
word-break: break-all;
165208
}
166209
}
167210
@@ -176,7 +219,7 @@ export default {
176219
177220
@media screen and (max-width: 700px) {
178221
.extra {
179-
phain-wrapper: 8px !important;
222+
padding-left: 8px !important;
180223
}
181224
182225
.cert-details {

0 commit comments

Comments
 (0)