Descrição da tarefa
Relacionado com scieloorg/opac_5#469
Objetivo
Implementar o novo comportamento da lista de autores exibida abaixo do título do artigo e do modal de autoria aberto ao clicar em cada autor.
A implementação deve usar JavaScript para montar:
- A lista de autores abaixo do título;
- O colapso da lista quando houver muitos autores;
- O modal com os dados do autor clicado;
- A indicação visual de autor correspondente;
- As seções complementares do corpo do artigo:
- Contribuição de autoria;
- Conflito de interesses;
- Editor Responsável.
Estrutura HTML esperada para a lista de autores
Abaixo do título do artigo deve existir um container vazio:
<div class="scielo__contribGroup">
<ul aria-label="Lista de autores" class="author-list" id="authorList"></ul>
</div>
Deve haver um único modal na página para a exibição dos detalhes de cada um dos autores:
<!-- Modal Authors -->
<div class="modal fade ModalDefault ModalTutors"
id="ModalTutors"
tabindex="-1"
role="dialog"
aria-modal="true"
aria-labelledby="ModalTutorsLabel"
aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h2 class="h4 modal-title" id="ModalTutorsLabel">
Authorship
</h2>
<button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Fechar">
</button>
</div>
<div class="modal-body">
<div id="authorCardsContainer"></div>
<div class="correspondence mt-3 mb-4 d-none"
id="correspondenceSection">
<div class="section-grid">
<span class="material-icons-outlined"
aria-hidden="true">
person
</span>
<div>
<p class="section-title mb-1">
Correspondence:
</p>
<div class="section-content">
<span id="correspondenceText"></span>
<br>
<a href=""
id="correspondenceEmail">
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
Javascript base esperado
const MAX_VISIBLE_AUTHORS = 25;
const ALWAYS_VISIBLE_FIRST = 2;
const ALWAYS_VISIBLE_LAST = 1;
const authorList = document.getElementById("authorList");
const authorModalElement = document.getElementById("ModalTutors");
const authorCardsContainer = document.getElementById("authorCardsContainer");
const correspondenceSection = document.getElementById("correspondenceSection");
const correspondenceText = document.getElementById("correspondenceText");
const correspondenceEmail = document.getElementById("correspondenceEmail");
const authorModal = authorModalElement
? new bootstrap.Modal(authorModalElement)
: null;
let expanded = false;
Estrutura de dados esperada
const authors = [
{
name: "John H. Livingston",
affiliations: ["1", "2"],
orcid: "0000-0002-4881-3620",
email: "[email protected]",
corresponding: true,
roles: "Conceptualization · Investigation · Writing – original article"
},
{
name: "Erik A. Petigura",
affiliations: ["3"],
orcid: "0000-0003-0967-2893",
email: "[email protected]",
corresponding: true,
roles: "Methodology · Data analysis"
}
];
As afiliações devem vir de um mapa separado
const affiliationMap = {
"1": "National Astronomical Observatory of Japan – Tokyo, Japan.",
"2": "Astrobiology Center – Tokyo, Japan.",
"3": "Department of Physics and Astronomy, University of California – Los Angeles, USA."
};
Função para criar o botão do autor
function createAuthorButton(author, index) {
const button = document.createElement("button");
button.type = "button";
button.className = "btn-link px-0";
button.setAttribute("data-author-index", index);
button.setAttribute("aria-label", `Abrir detalhes de ${author.name}`);
if (author.corresponding || author.email) {
const icon = document.createElement("span");
icon.className = "material-icons-outlined me-1 fs-6";
icon.setAttribute("aria-hidden", "true");
icon.textContent = "mail";
button.appendChild(icon);
}
const name = document.createElement("span");
name.textContent = author.name;
button.appendChild(name);
button.addEventListener("click", () => {
openAuthorModal(index);
});
return button;
}
Renderização da lista de autores
function appendAuthorItem(author, index, isLastVisibleItem) {
const li = document.createElement("li");
li.appendChild(createAuthorButton(author, index));
if (!isLastVisibleItem) {
const separator = document.createElement("span");
separator.className = "author-separator";
separator.setAttribute("aria-hidden", "true");
separator.textContent = ",";
li.appendChild(separator);
}
authorList.appendChild(li);
}
Botão para expandir ou ocultar autores
function appendSummaryItem(hiddenCount, isLastItem = false) {
const li = document.createElement("li");
const button = document.createElement("button");
button.type = "button";
button.className = "btn btn-secondary btn-sm outlineFadeLink ms-0";
if (expanded) {
button.textContent = "Ocultar autores";
button.setAttribute("aria-label", "Ocultar autores intermediários");
button.setAttribute("aria-expanded", "true");
} else {
button.textContent = `+ ${hiddenCount} autores`;
button.setAttribute("aria-label", `Mostrar os ${hiddenCount} autores ocultos`);
button.setAttribute("aria-expanded", "false");
}
button.addEventListener("click", () => {
expanded = !expanded;
renderAuthors();
});
li.appendChild(button);
if (!isLastItem) {
const separator = document.createElement("span");
separator.className = "author-separator";
separator.setAttribute("aria-hidden", "true");
separator.textContent = ",";
li.appendChild(separator);
}
authorList.appendChild(li);
}
Lógica de colapso da lista
function renderAuthors() {
if (!authorList) return;
authorList.innerHTML = "";
const total = authors.length;
const hiddenCount = total - (ALWAYS_VISIBLE_FIRST + ALWAYS_VISIBLE_LAST);
const canCollapse = total > MAX_VISIBLE_AUTHORS && hiddenCount > 0;
if (!canCollapse) {
authors.forEach((author, index) => {
appendAuthorItem(author, index, index === total - 1);
});
return;
}
if (expanded) {
authors.forEach((author, index) => {
appendAuthorItem(author, index, index === total - 1);
});
appendSummaryItem(hiddenCount, true);
return;
}
const firstAuthors = authors.slice(0, ALWAYS_VISIBLE_FIRST);
const lastAuthors = authors.slice(total - ALWAYS_VISIBLE_LAST);
firstAuthors.forEach((author, index) => {
appendAuthorItem(author, index, false);
});
appendSummaryItem(hiddenCount, false);
lastAuthors.forEach((author, idx) => {
const realIndex = total - ALWAYS_VISIBLE_LAST + idx;
appendAuthorItem(author, realIndex, true);
});
}
Montagem do conteúdo do modal
function buildInstitutionText(author) {
return author.affiliations
.map(code => affiliationMap[code] || `Afiliação ${code}`)
.join(" ");
}
function buildAuthorCard(author) {
const card = document.createElement("div");
card.className = "author-card";
const nameRow = document.createElement("div");
nameRow.className = "author-grid-row";
const personIcon = document.createElement("span");
personIcon.className = "material-icons-outlined";
personIcon.setAttribute("aria-hidden", "true");
personIcon.textContent = "person";
const nameWrapper = document.createElement("div");
nameWrapper.className = "author-name";
const nameText = document.createElement("span");
nameText.className = "author-name-text";
nameText.textContent = author.name;
nameWrapper.appendChild(nameText);
nameRow.appendChild(personIcon);
nameRow.appendChild(nameWrapper);
card.appendChild(nameRow);
const roles = document.createElement("div");
roles.className = "author-roles author-subrow mb-2";
roles.textContent = author.roles || "Sem funções informadas.";
card.appendChild(roles);
const institutionRow = document.createElement("div");
institutionRow.className = "author-grid-row author-subrow mb-2";
const schoolIcon = document.createElement("span");
schoolIcon.className = "material-icons-outlined";
schoolIcon.setAttribute("aria-hidden", "true");
schoolIcon.textContent = "school";
const institution = document.createElement("div");
institution.className = "author-institution";
const institutionText = document.createElement("span");
institutionText.className = "author-inst-text";
institutionText.textContent = buildInstitutionText(author);
institution.appendChild(institutionText);
institutionRow.appendChild(schoolIcon);
institutionRow.appendChild(institution);
card.appendChild(institutionRow);
if (author.orcid) {
const orcidWrap = document.createElement("div");
orcidWrap.className = "orcid-button-wrap ms-4";
const orcidLink = document.createElement("a");
orcidLink.target = "_blank";
orcidLink.rel = "noopener noreferrer";
orcidLink.className = "btn btn-secondary orcid-button";
orcidLink.href = `https://orcid.org/${author.orcid}`;
orcidLink.textContent = author.orcid;
orcidLink.setAttribute(
"aria-label",
`Acessar perfil ORCID ${author.orcid}. Abre em nova aba.`
);
orcidWrap.appendChild(orcidLink);
card.appendChild(orcidWrap);
}
return card;
}
Abertura do modal
function openAuthorModal(index) {
if (!authorModal || !authorCardsContainer) return;
const author = authors[index];
if (!author) return;
authorCardsContainer.innerHTML = "";
authorCardsContainer.appendChild(buildAuthorCard(author));
if (author.email) {
correspondenceSection.classList.remove("d-none");
correspondenceText.textContent = `${author.name}. E-mail: `;
correspondenceEmail.href = `mailto:${author.email}`;
correspondenceEmail.textContent = author.email;
} else {
correspondenceSection.classList.add("d-none");
correspondenceText.textContent = "";
correspondenceEmail.removeAttribute("href");
correspondenceEmail.textContent = "";
}
authorModal.show();
}
renderAuthors();
Subtarefas
<a class="list-group-item-action d-block" href="#articleSection3.7">
Contribuição de autoria
</a>
E no corpo:
<div class="articleSection" id="articleSection3.7">
<h2 class="h5">Contribuição de autoria</h2>
</div>
Essa seção deve usar os mesmos dados da lista de autores e do modal, principalmente:
<a class="list-group-item-action d-block" href="#articleSection3.8">
Conflito de interesses
</a>
E no corpo:
<a class="list-group-item-action d-block" href="#articleSection3.8">
Conflito de interesses
</a>
O texto deve refletir o dado real do artigo.
<a class="list-group-item-action d-block" href="#articleSection3.9">
Editor Responsável
</a>
E no corpo:
<div class="articleSection" id="articleSection3.9">
<h2 class="h5">Editor Responsável</h2>
<p>Nome do editor responsável</p>
</div>
Requisitos de acessibilidade:
- O modal deve seguir o padrão ModalDefault usado no SciELO.
- O modal deve possuir role="dialog".
- O modal deve possuir aria-modal="true".
- O modal deve usar aria-labelledby apontando para o título.
- O botão de fechar deve possuir aria-label.
- O gerenciamento de foco deve ser feito pelo Bootstrap.
- Não remover foco manualmente via JavaScript.
- Os nomes dos autores devem ser botões.
- Não usar href="#" para abrir modal.
- O botão + N autores deve usar aria-expanded.
- Ícones decorativos devem usar aria-hidden="true".
- Links ORCID devem usar target="_blank" e rel="noopener noreferrer".
- Links ORCID devem informar no aria-label que abrem em nova aba.
Critérios de aceite
Considerações e notas
- Não devem ser carregadas no packtools classes css dentro de <style>. Todo o CSS deve vir do Design System.
- Verifique este link como referencia de html funcional.
Descrição da tarefa
Relacionado com scieloorg/opac_5#469
Objetivo
Implementar o novo comportamento da lista de autores exibida abaixo do título do artigo e do modal de autoria aberto ao clicar em cada autor.
A implementação deve usar JavaScript para montar:
Estrutura HTML esperada para a lista de autores
Abaixo do título do artigo deve existir um container vazio:
Deve haver um único modal na página para a exibição dos detalhes de cada um dos autores:
Javascript base esperado
Estrutura de dados esperada
As afiliações devem vir de um mapa separado
Função para criar o botão do autor
Renderização da lista de autores
Botão para expandir ou ocultar autores
Lógica de colapso da lista
Montagem do conteúdo do modal
Abertura do modal
Subtarefas
Além da lista de autores e do modal, o artigo deve conter as seguintes seções quando os dados existirem.
Contribuição de autoria
Deve aparecer no menu de navegação do artigo:
E no corpo:
Essa seção deve usar os mesmos dados da lista de autores e do modal, principalmente:
Deve aparecer no menu:
E no corpo:
O texto deve refletir o dado real do artigo.
Deve aparecer no menu:
E no corpo:
Requisitos de acessibilidade:
Critérios de aceite
Considerações e notas