Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/core/templates/components/feedback.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ <h2 class="feedback__heading" dir="auto">{% trans 'Was this page helpful?' %}</h
</button>
</div>
</div>
<div class="feedback__status" role="status" tabindex="-1" dir="auto" data-feedback-status></div>

<div class="feedback__form" data-additional-feedback-container>
<div class="feedback__form-container">
Expand Down
10 changes: 10 additions & 0 deletions apps/core/tests/test_feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@ def test_update_feedback_happy(self):
self.assertEqual(feedback_obj.feedback, "happy")
self.assertEqual(feedback_obj.page.specific, self.target_page)

def test_status_region_rendered_before_feedback(self):
# The confirmation message is announced through this live region, so it
# needs to be on the page before the feedback buttons are used.
res = self.client.get(self.target_page.url)
self.assertContains(
res,
'<div class="feedback__status" role="status" tabindex="-1" dir="auto" data-feedback-status></div>',
html=True,
)

def test_csrf_token_exists(self):
cookie = self.client.cookies.get(settings.CSRF_COOKIE_NAME)
self.assertIsNone(cookie)
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/static_src/js/active-nav-item/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const initActiveNavItem = () => {
links.forEach((link) => {
if (new URL(link.href).pathname === window.location.pathname) {
link.classList.add('active');
link.setAttribute('aria-current', 'page');
}
});
};
8 changes: 8 additions & 0 deletions apps/frontend/static_src/js/active-nav-item/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,12 @@ describe('initActiveNavItem', () => {
expect(concepts.classList.contains('active')).toBe(false);
expect(guides.classList.contains('active')).toBe(true);
});

it('marks only the link matching the current path as the current page', () => {
initActiveNavItem();

const [concepts, guides] = document.querySelectorAll('.navigation__link');
expect(concepts.hasAttribute('aria-current')).toBe(false);
expect(guides.getAttribute('aria-current')).toBe('page');
});
});
10 changes: 7 additions & 3 deletions apps/frontend/static_src/js/feedback/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const handleFeedback = () => {
const happyButton = document.querySelector('[data-happy-button]');
const unhappyButton = document.querySelector('[data-unhappy-button]');
const feedbackContainer = document.querySelector('[data-feedback-container]');
const feedbackStatus = document.querySelector('[data-feedback-status]');
const additionalFeedbackContainer = document.querySelector(
'[data-additional-feedback-container]',
);
Expand All @@ -41,9 +42,11 @@ export const handleFeedback = () => {
tooltipList.forEach((tooltip) => {
tooltip.dispose();
});
feedbackContainer.innerHTML = `<span dir="auto">${gettext(
'Thanks for your feedback!',
)}</span>`;
// Announce the confirmation in the live region, and move focus to it
// before removing the buttons so focus isn't lost to the page body.
feedbackStatus.textContent = gettext('Thanks for your feedback!');
feedbackStatus.focus();
feedbackContainer.remove();
feedbackPk = data.pk;
additionalFeedbackContainer.classList.add('active');
} catch (err) {
Expand All @@ -64,6 +67,7 @@ export const handleFeedback = () => {
'Content-type': 'application/json; charset=UTF-8',
},
});
feedbackStatus.focus();
additionalFeedbackContainer.innerHTML = '';
} catch (err) {
console.log(err);
Expand Down
86 changes: 86 additions & 0 deletions apps/frontend/static_src/js/feedback/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
afterEach,
beforeEach,
describe,
expect,
it,
vi,
} from 'vite-plus/test';
import { handleFeedback } from '.';

describe('handleFeedback', () => {
let fetchMock;

beforeEach(() => {
vi.stubGlobal('gettext', (text) => text);
fetchMock = vi.fn(async () => ({ json: async () => ({ pk: 1 }) }));
vi.stubGlobal('fetch', fetchMock);

document.body.innerHTML = `
<div class="feedback">
<input type="hidden" name="csrfmiddlewaretoken" value="test-csrf-token">
<div data-feedback-container>
<h2>Was this page helpful?</h2>
<button data-bs-toggle="tooltip" data-bs-title="This page was helpful" data-happy-button aria-label="Happy"></button>
<button data-bs-toggle="tooltip" data-bs-title="This page could be improved" data-unhappy-button aria-label="Unhappy"></button>
</div>
<div role="status" tabindex="-1" data-feedback-status></div>
<div data-additional-feedback-container>
<label for="feedback">Your feedback</label>
<textarea data-feedback-text id="feedback"></textarea>
<button data-submit-button>Submit</button>
</div>
</div>
`;
handleFeedback();
});

afterEach(() => {
document.body.innerHTML = '';
vi.unstubAllGlobals();
});

it.each(['happy', 'unhappy'])(
'announces %s feedback in the existing status region and moves focus to it',
async (feedback) => {
const status = document.querySelector('[role="status"]');
const button = document.querySelector(`[data-${feedback}-button]`);
button.focus();
button.click();

await vi.waitFor(() => {
expect(status.textContent).toBe('Thanks for your feedback!');
});
// The same element that was on the page before the update, so screen
// readers already track it as a live region.
expect(status.isConnected).toBe(true);
expect(document.activeElement).toBe(status);
expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({
feedback,
});
},
);

it('keeps focus on the confirmation after submitting a comment', async () => {
const status = document.querySelector('[role="status"]');
document.querySelector('[data-unhappy-button]').click();
await vi.waitFor(() => {
expect(document.activeElement).toBe(status);
});

document.querySelector('[data-feedback-text]').value = 'Add a screenshot';
const submit = document.querySelector('[data-submit-button]');
submit.focus();
submit.click();

await vi.waitFor(() => {
expect(submit.isConnected).toBe(false);
});
expect(document.activeElement).toBe(status);
expect(status.isConnected).toBe(true);
expect(JSON.parse(fetchMock.mock.calls[1][1].body)).toEqual({
pk: 1,
feedback_text: 'Add a screenshot',
});
});
});
5 changes: 5 additions & 0 deletions apps/frontend/static_src/scss/components/feedback.scss
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
width: 42px;
}

&__status {
text-align: center;
margin-bottom: $gutter;
}

&__form {
visibility: hidden;
opacity: 0;
Expand Down