-
Notifications
You must be signed in to change notification settings - Fork 295
feat: improved accessibility of notification tray #1817
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
feat: improved accessibility of notification tray #1817
Conversation
|
Thanks for the pull request, @filippovskii09! This repository is currently maintained by Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review. 🔘 Get product approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. 🔘 Update the status of your PRYour PR is currently marked as a draft. After completing the steps above, update its status by clicking "Ready for Review", or removing "WIP" from the title, as appropriate. Where can I find more information?If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources: When can I expect my changes to be merged?Our goal is to get community contributions seen and reviewed as efficiently as possible. However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1817 +/- ##
==========================================
+ Coverage 90.83% 91.26% +0.43%
==========================================
Files 345 350 +5
Lines 5791 5872 +81
Branches 1380 1393 +13
==========================================
+ Hits 5260 5359 +99
+ Misses 512 494 -18
Partials 19 19 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
| } | ||
|
|
||
| // Tab | ||
| const courseOutlineTrigger = document.querySelector('#courseOutlineTrigger'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[optional]: Maybe it would be better to clarify what exactly this is a sidebar trigger.
| const courseOutlineTrigger = document.querySelector('#courseOutlineTrigger'); | |
| const courseOutlineSidebarTrigger = document.querySelector('#courseOutlineTrigger'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, this naming better, fixed
| const leftArrow = document.querySelector('.previous-button'); | ||
| if (leftArrow && !leftArrow.disabled) { | ||
| event.preventDefault(); | ||
| leftArrow.focus(); | ||
| return; | ||
| } | ||
|
|
||
| const rightArrow = document.querySelector('.next-button'); | ||
| if (rightArrow && !rightArrow.disabled) { | ||
| event.preventDefault(); | ||
| rightArrow.focus(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[question]: These are two almost identical constructs. Can we create a function for this that would be convenient to reuse here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thx, create helper function for this, fixed
| focusSidebarTriggerBtn(); | ||
| }; | ||
|
|
||
| const handleKeyDown = useCallback((event) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit]: I think it's worth adding a meaningful comment here that would describe how the logic of moving focus around the page works and for which elements.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i added describe text for function, please check it
| <div className="d-flex align-items-center mb-2"> | ||
| <strong className="p-2.5 d-inline-block course-sidebar-title">{title}</strong> | ||
| {/* TODO: view this title in UI and decide */} | ||
| {/* <strong className="p-2.5 d-inline-block course-sidebar-title">{title}</strong> */} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need these comments?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed
| const newFocusStatus = !isOpenNotificationStatusBar; | ||
| setSessionStorage(`notificationTrayFocus.${courseId}`, String(newFocusStatus)); | ||
|
|
||
| const isNotificationTrayOpen = getSessionStorage(`notificationTrayStatus.${courseId}`) === 'open'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit]: I suggest splitting the logic into two PRs:
- Improve accessibility issues
- Fix bug with storing sidebar state with notifications
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, i removed Fixes for storing sidebar state
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR for storing sidebar state: #1823
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i added this custom hook useSidebarFocusAndKeyboard.js to separate the logic which we added in this PR
because a lot of logic interfered with orientation in the component
| import { renderHook, act } from '@testing-library/react'; | ||
| import SidebarContext from '../../SidebarContext'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| import { renderHook, act } from '@testing-library/react'; | |
| import SidebarContext from '../../SidebarContext'; | |
| import { renderHook, act } from '@testing-library/react'; | |
| import SidebarContext from '../../SidebarContext'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved
| @@ -0,0 +1,152 @@ | |||
| import React from 'react'; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this React import?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed
| import { Factory } from 'rosie'; | ||
| import { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[code style]: Let’s separate external library imports from local imports with a blank line for better readability.
| import { Factory } from 'rosie'; | |
| import { | |
| import { Factory } from 'rosie'; | |
| import { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the line import { createRef } from 'react'; come before import { Factory } from 'rosie';? The idea is to separate library imports from local imports.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
| import { Factory } from 'rosie'; | ||
| import messages from '../../../messages'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[question]: Does it make sense to use aliases here?
| import { Factory } from 'rosie'; | |
| import messages from '../../../messages'; | |
| import { Factory } from 'rosie'; | |
| import messages from '../../../messages'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
| import PropTypes from 'prop-types'; | ||
| import { Factory } from 'rosie'; | ||
|
|
||
| import messages from '../../../messages'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[question]: Does it make sense to use aliases here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
| }); | ||
|
|
||
| expect(mockToggleSidebar).toHaveBeenCalledWith(null); | ||
| act(() => { jest.runAllTimers(); }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| act(() => { jest.runAllTimers(); }); | |
| act(() => jest.runAllTimers()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
|
|
||
| const mockUseSidebarFocusAndKeyboard = useSidebarFocusAndKeyboard; | ||
|
|
||
| describe('SidebarBase (Refactored)', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| describe('SidebarBase (Refactored)', () => { | |
| describe('SidebarBase', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
| } | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [sidebarId, toggleSidebar]); | ||
| }, [toggleSidebar]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[clarify]: Is there any reason why sidebarId was removed from deps? Also, do I need to remove // eslint-disable-next-line react-hooks/exhaustive-deps?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no reason, removed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My question is related to the fact that sidebarId was previously included in the dependency array. Why was it removed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
because sidebarId it was an unnecessary dependency
| const triggerButton = screen.getByRole('button', { | ||
| name: messages.openNotificationTrigger.defaultMessage, | ||
| }); | ||
| fireEvent.click(triggerButton); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets use userEvent
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
| sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations', | ||
| sequenceId: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions', | ||
|
|
||
| currentSequence: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just curious. Why was this change only added now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my mistake, removed
47e07d5 to
217a46c
Compare
| import { tryFocusAndPreventDefault } from '../../utils'; | ||
| import SidebarContext from '../../SidebarContext'; | ||
|
|
||
| export const useSidebarFocusAndKeyboard = (sidebarId, triggerButtonSelector = '.sidebar-trigger-btn') => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved
| import { renderHook, act } from '@testing-library/react'; | ||
| import SidebarContext from '../../SidebarContext'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved
| import { Factory } from 'rosie'; | ||
| import { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the line import { createRef } from 'react'; come before import { Factory } from 'rosie';? The idea is to separate library imports from local imports.
|
|
||
| it('should hide title bar when showTitleBar prop is false', () => { | ||
| renderSidebar({ currentSidebar: SIDEBAR_ID }, { showTitleBar: false }); | ||
| expect(screen.queryByText('Test Sidebar Title')).not.toBeInTheDocument(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we replace the static text in these tests with values from defaultProps?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
| } | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [sidebarId, toggleSidebar]); | ||
| }, [toggleSidebar]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My question is related to the fact that sidebarId was previously included in the dependency array. Why was it removed?
| import { Factory } from 'rosie'; | ||
| import messages from '@src/courseware/course/messages'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| import { Factory } from 'rosie'; | |
| import messages from '@src/courseware/course/messages'; | |
| import { Factory } from 'rosie'; | |
| import messages from '@src/courseware/course/messages'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
| @@ -0,0 +1,9 @@ | |||
| export const tryFocusAndPreventDefault = (event, selector) => { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolved
|
Sandbox deployment successful 🚀 |
|
Sandbox deployment successful 🚀 |
|
Hey @PKulkoRaccoonGang, am I right in assuming that this is another PR you'll add when you create your product proposal for a11y improvements? |
|
|
||
| mockQuerySelector = jest.spyOn(document, 'querySelector'); | ||
| mockQuerySelector.mockImplementation((selector) => { | ||
| if (selector === TRIGGER_SELECTOR) { return triggerButtonMock; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if (selector === TRIGGER_SELECTOR) { return triggerButtonMock; } | |
| if (selector === TRIGGER_SELECTOR) { | |
| return triggerButtonMock; | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
| if (selector === '#courseOutlineSidebarTrigger') { return mockOutlineTrigger; } | ||
| if (selector === '.previous-button') { return mockPrevButton; } | ||
| if (selector === '.next-button') { return mockNextButton; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if (selector === '#courseOutlineSidebarTrigger') { return mockOutlineTrigger; } | |
| if (selector === '.previous-button') { return mockPrevButton; } | |
| if (selector === '.next-button') { return mockNextButton; } | |
| if (selector === '#courseOutlineSidebarTrigger') { | |
| return mockOutlineTrigger; | |
| } | |
| if (selector === '.previous-button') { | |
| return mockPrevButton; | |
| } | |
| if (selector === '.next-button') { | |
| return mockNextButton; | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
| const { result } = renderHookWithContext(mockContextValue); | ||
|
|
||
| mockQuerySelector.mockImplementation((selector) => { | ||
| if (selector === TRIGGER_SELECTOR) { return triggerButtonMock; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if (selector === TRIGGER_SELECTOR) { return triggerButtonMock; } | |
| if (selector === TRIGGER_SELECTOR) { | |
| return triggerButtonMock; | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
| if (selector === '.call-to-action-btn') { return mockCtaButton; } | ||
| if (selector === TRIGGER_SELECTOR) { return triggerButtonMock; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if (selector === '.call-to-action-btn') { return mockCtaButton; } | |
| if (selector === TRIGGER_SELECTOR) { return triggerButtonMock; } | |
| if (selector === '.call-to-action-btn') { | |
| return mockCtaButton; | |
| } | |
| if (selector === TRIGGER_SELECTOR) { | |
| return triggerButtonMock; | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
| sidebarTriggerBtnRef.current?.blur(); | ||
| sidebarTriggerBtnRef.current?.blur(); | ||
|
|
||
| sidebarTriggerBtnRef.current?.blur(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[clarify]: Do we need to add blur to sidebarTriggerBtnRef three times here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed extra lines
|
Sandbox deployment failed 💥 |
|
Sandbox deployment failed 💥 |
|
Sandbox deployment successful 🚀 |
Note
This PR will be added to the product proposal to improve accessibility.
Description
This PR introduces significant accessibility (a11y) improvements to the Notification Tray (Sidebar) and its Trigger Button, making the feature fully operable for keyboard-only users.
Key Changes
Trigger State
aria-expandedattribute to announce its state (open/closed) to assistive technologies.Focus Management
Managed Keyboard Navigation
Tabkey behavior when the sidebar is open.Steps to Test
Note: These steps should be performed using the keyboard to verify accessibility.
Tabkey repeatedly to navigate through the page's interactive elements.Enterto activate the trigger.Enteragain to close the tray.Before:
2025-10-31.16.00.26.mov
After:
2025-10-30.13.47.42.mov