Skip to content
This repository has been archived by the owner on May 24, 2024. It is now read-only.

[terra-slide-panel] Disclosing node capturing method update #2095

Merged
merged 11 commits into from
Mar 26, 2024
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this right? This seems like it would result in an accessibility violation because there's not enough contrast.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the highlighted style was updated? May need UX to confirm if this is the correct styling.

Copy link
Contributor Author

@adoroshk adoroshk Mar 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDIO screenshots for terra-agregator test example which uses slide panel were updated, as with new functionality added, the disclosing node in terra-agregator received the focus. However, the focus style itself hasn't changed, it just wasn't receiving focus before as Slide Panel had no means to return the focus to that node. The affected example is a test example for some other functionality, it is not presented on the doc page and is not relevant to anything important. I don't think we should fix it or log a bug.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion packages/terra-framework-docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
* Changed
* Updated `terra-navigation-side-menu` example.

* Added
* Added test example for `terra-slide-panel` with no `mainContent` prop.

## 1.75.0 - (March 19, 2024)

* Changed
Expand All @@ -13,7 +16,7 @@
## 1.74.0 - (March 14, 2024)

* Added
* Added an example of a `compact-interactive-list` with no borders.
* Added an example for `terra-compact-interactive-list` with no borders.

* Changed
* Updated `terra-compact-interactive-list` documentation for `terra-menu` support.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React, { useState } from 'react';
import SlidePanel, { SlidePanelPositions } from 'terra-slide-panel';
import classNames from 'classnames/bind';
import styles from './SlidePanelNoMainContent.test.module.scss';

const cx = classNames.bind(styles);

const mainContentForSlidePanel = (togglePanelHandler) => (
<div>
<header className={cx('header-content')}>
<h3>Main Content</h3>
<button id="mainToggleBtn" type="button" onClick={togglePanelHandler} className={cx('custom-button')}>Toggle Panel 1</button>
{/* eslint-disable-next-line jsx-a11y/no-interactive-element-to-noninteractive-role */}
<button id="mainToggleBtnWithWrongRole" type="button" role="presentation" onClick={togglePanelHandler} className={cx('extension-button')}>Toggle Panel 2</button>
{ /* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions */}
<p id="mainToggleParagraph" onClick={togglePanelHandler} className={cx('extension-button')}>Toggle Panel 3</p>
</header>
<div className={cx('content-wrapper')}>
<p>
This is the main content area of the slide panel.
The overall height of the SlidePanel is determined by
the intrinsic height of the content in this container.
</p>
<p>
{'Focus is moved to the toggle button in the panel container when the panel is opened via the componentDidUpdate lifecycle hook in '}
<a href="https://github.com/cerner/terra-framework/blob/main/packages/terra-slide-panel/src/terra-dev-site/doc/example/DefaultSlidePanel.jsx">the example code</a>
.
</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
<li>Item 5</li>
<li>Item 6</li>
<li>Item 7</li>
<li>Item 8</li>
</ul>
</div>
</div>
);

const panelContentForSlidePanel = (togglePanelHandler) => (
<div>
<header className={cx('header-content')}>
<h3 className={cx('heading')}>Panel Content</h3>
<button id="panelToggleBtn" type="button" onClick={togglePanelHandler} className={cx('extension-button')}>Toggle Panel</button>
</header>
<div className={cx('content-wrapper')}>
<p>This is the panel content area of the slide panel.</p>
<p>
{'Focus is moved to the toggle button in the main container when the panel is closed via the componentDidUpdate lifecycle hook in '}
<a href="https://github.com/cerner/terra-framework/blob/main/packages/terra-slide-panel/src/terra-dev-site/doc/example/DefaultSlidePanel.jsx">the example code</a>
.
</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
<li>Item 5</li>
<li>Item 6</li>
<li>Item 7</li>
<li>Item 8</li>
<li>Item 9</li>
<li>Item 10</li>
<li>Item 11</li>
<li>Item 12</li>
<li>Item 13</li>
<li>Item 14</li>
<li>Item 15</li>
</ul>
</div>
</div>
);

const SlidePanelNoMainContent = () => {
const [isOpen, setIsOpen] = useState(false);

const handlePanelToggle = () => {
setIsOpen(!isOpen);
};

return (
<div className={cx('example-container')}>
{mainContentForSlidePanel(handlePanelToggle)}
<div className={isOpen ? cx('panel-container') : null}>
<SlidePanel
id="test-slide"
panelContent={panelContentForSlidePanel(handlePanelToggle)}
panelAriaLabel="Panel content area"
panelSize="small"
panelBehavior="overlay"
panelPosition={SlidePanelPositions.END}
isOpen={isOpen}
/>
</div>
</div>
);
};

export default SlidePanelNoMainContent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
:local {
.example-container {
position: relative;
}

.panel-container > div:first-child {
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
}

.header-content {
background-color: #d3d3d3;

p {
width: auto;
display: inline-block;
margin: 0 !important;
}
}

.header-content > :not(:last-child){
margin-right: 5px;
}

.header-content h3 {
display: inline-block;
margin: 0;
padding: 5px;
}

.content-wrapper {
margin: 5px;
}

.custom-button {
display: inline-block;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -364,8 +364,6 @@ exports[`SlidePanelManager should disclose content in SlidePanel 1`] = `
aria-hidden="false"
className="main"
key="main"
onClick={[Function]}
onKeyUp={[Function]}
role="main"
tabIndex="-1"
>
Expand Down Expand Up @@ -863,8 +861,6 @@ exports[`SlidePanelManager should render a SlidePanelManager with level three he
aria-hidden="false"
className="main"
key="main"
onClick={[Function]}
onKeyUp={[Function]}
role="main"
tabIndex="-1"
>
Expand Down Expand Up @@ -1130,8 +1126,6 @@ exports[`SlidePanelManager should render the SlidePanelManager with custom props
aria-hidden="false"
className="main"
key="main"
onClick={[Function]}
onKeyUp={[Function]}
role="main"
tabIndex="-1"
>
Expand Down Expand Up @@ -1390,8 +1384,6 @@ exports[`SlidePanelManager should render the SlidePanelManager with defaults 1`]
aria-hidden="false"
className="main"
key="main"
onClick={[Function]}
onKeyUp={[Function]}
role="main"
tabIndex="-1"
>
Expand Down Expand Up @@ -1650,8 +1642,6 @@ exports[`SlidePanelManager should render the SlidePanelManager with squish overr
aria-hidden="false"
className="main"
key="main"
onClick={[Function]}
onKeyUp={[Function]}
role="main"
tabIndex="-1"
>
Expand Down
3 changes: 3 additions & 0 deletions packages/terra-slide-panel/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

* Changed
* Updated capturing the disclosing node to accommodate for cases with no `mainContent`

## 3.47.0 - (December 18, 2023)

* Changed
Expand Down
57 changes: 43 additions & 14 deletions packages/terra-slide-panel/src/SlidePanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,33 +85,65 @@ const defaultProps = {
panelSize: 'small',
};

/**
* This method ensures that aria-expanded attribute is allowed with the node role to avoid accessibility violations.
* More info on associated roles can be found here: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-expanded#associated_roles
* @param {HTMLElement} node - a node to check
* @returns true if node's role is compatible with aria-expanded attribute
*/
const roleIsCompartible = (node) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does compartible mean?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It checks if node's role is one compatible (sorry for typo, will fix if we won't remove it at the end) with aria-expanded attribute. Here is the documentation for roles associated with aria-expanded attribute.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, it is just a typo :). I still don't think we need a method like this though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

const role = node.getAttribute('role');
return (
role === 'application'
|| role === 'button'
|| role === 'checkbox'
|| role === 'combobox'
|| role === 'gridcell'
|| role === 'link'
|| role === 'listbox'
|| role === 'menuitem'
|| role === 'row'
|| role === 'rowheader'
|| role === 'tab'
|| role === 'treeitem'
|| role === null
|| role === undefined
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a lot of conditions that will always be sequentially checked. Can we optimize it using a Set and the Set.has method?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And I'm confused why are we testing for all the possible values if we don't care that role is null/undefined?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sdadn aria-expanded attribute should be used only with specific roles, otherwise it causes accessibility violation. This attribute was not introduced in this change, but it was causing accessibility violations with specific roles only, so I added a check. I'll double check with @mjpalazzo what would be the best way to address that

};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not need a method like this. It makes me feel like we might be doing something incorrectly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed, the role in question changed in the example


class SlidePanel extends React.Component {
constructor(props) {
super(props);
this.setPanelNode = this.setPanelNode.bind(this);
this.mainNode = React.createRef();
this.setLastClicked = this.setLastClicked.bind(this);
this.setDisclosingNode = this.setDisclosingNode.bind(this);
this.mainAriaDescribedByID = `detail-panel-warning-${uuidv4()}`;
}

componentDidUpdate(prevProps) {
if (this.props.isOpen && this.props.isOpen !== prevProps.isOpen) {
// Save the disclosing node for returning focus when panel is closed
this.setDisclosingNode(this.lastClicked);
this.setDisclosingNode(document.activeElement);
this.panelNode.focus();
return;
}

if (!this.props.isOpen && this.props.isOpen !== prevProps.isOpen) {
if (this.disclosingNode?.focus) {
// Return focus to the disclosing element
this.disclosingNode.setAttribute('aria-expanded', 'false');
if (this.disclosingNode.getAttribute('aria-expanded')) {
// Set aria-expanded attribute to false for nodes that had that attribute only
this.disclosingNode.setAttribute('aria-expanded', 'false');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not clear on why we need to set the aria-expanded attribute on the disclosing node. That element is not being expanded.

Copy link
Contributor Author

@adoroshk adoroshk Mar 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aria-expanded attribute was not added in this change, it was added before. I'll check with @mjpalazzo if it can be removed.

}
this.disclosingNode.focus();
return;
}
// The disclosing element doesn't exist or isn't focusable, return focus to the main div
this.mainNode.current.focus();
// The disclosing element doesn't exist or isn't focusable, return focus to the main div or body
if (this.mainNode?.current) {
this.mainNode.current.focus();
} else {
document.body.focus();
}
}
}

Expand All @@ -122,13 +154,12 @@ class SlidePanel extends React.Component {
this.panelNode = node;
}

setLastClicked(event) {
this.lastClicked = event.target;
}

setDisclosingNode(node) {
if (node) {
node.setAttribute('aria-expanded', 'true');
if (roleIsCompartible(node)) {
// Set aria-expanded attribute to true for nodes with associated roles or no role only
node.setAttribute('aria-expanded', 'true');
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This node is not being expanded. I don't think we should be adding this role.

Copy link
Contributor Author

@adoroshk adoroshk Mar 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aria-expanded attribute wasn't added by me, it was there before the change, I just added a role check, as it can be added only with specific roles, otherwise it causes accessibility violation. I'll double check with @mjpalazzo if aria-expanded attribute needs to be removed.

this.disclosingNode = node;
}
}
Expand Down Expand Up @@ -189,8 +220,6 @@ class SlidePanel extends React.Component {
aria-hidden={isOpen && isFullscreen ? 'true' : 'false'}
ref={this.mainNode}
role="main"
onClick={this.setLastClicked}
onKeyUp={this.setLastClicked}
>
<VisuallyHiddenText
tabIndex="-1"
Expand All @@ -204,11 +233,11 @@ class SlidePanel extends React.Component {
const content = (panelPosition === SlidePanelPositions.START) ? (
<React.Fragment>
{panelDiv}
{mainDiv}
{mainContent && mainDiv}
</React.Fragment>
) : (
<React.Fragment>
{mainDiv}
{mainContent && mainDiv}
{panelDiv}
</React.Fragment>
);
Expand Down
17 changes: 13 additions & 4 deletions packages/terra-slide-panel/tests/jest/SlidePanel.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,15 @@ describe('When a SlidePanel is rendered', () => {
expect(wrapper).toMatchSnapshot();
});

it('should render a SlidePanel without main content if mainContent prop was not provided', () => {
const slidePanel = (
<SlidePanel />
);
const wrapper = enzymeIntl.shallowWithIntl(slidePanel).dive();
const mainContent = wrapper.find('.main');
expect(mainContent.length).toBe(0);
});

it('should render a SlidePanel with panel content', () => {
const slidePanel = (
<SlidePanel
Expand Down Expand Up @@ -138,7 +147,7 @@ describe('When a SlidePanel is rendered', () => {
});

it('should render a SlidePanel with panelAriaLabel and mainAriaLabel specified', () => {
const slidePanel = <SlidePanel panelAriaLabel="Panel content area" mainAriaLabel="Main content area" />;
const slidePanel = <SlidePanel panelAriaLabel="Panel content area" mainContent={<p>Main Content</p>} mainAriaLabel="Main content area" />;
const wrapper = enzymeIntl.shallowWithIntl(slidePanel).dive();

const panelDiv = wrapper.find('.panel');
Expand All @@ -163,15 +172,15 @@ describe('When a SlidePanel is rendered', () => {
});

it('should contain a Visually Hidden Text as the aria-describedby target', () => {
const slidePanel = <SlidePanel />;
const slidePanel = <SlidePanel mainContent={<p>Main Content</p>} />;
const wrapper = enzymeIntl.shallowWithIntl(slidePanel).dive();

const mainDiv = wrapper.find('#detail-panel-warning-00000000-0000-0000-0000-000000000000').prop('text');
expect(mainDiv).toEqual('Terra.slidePanel.discloseWarning');
});

it('should set the text property of Visually Hidden Text from ./translations', () => {
const slidePanel = <IntlProvider locale="en" messages={translationsFile}><SlidePanel /></IntlProvider>;
const slidePanel = <IntlProvider locale="en" messages={translationsFile}><SlidePanel mainContent={<p>Main Content</p>} /></IntlProvider>;
const wrapper = enzymeIntl.shallowWithIntl(slidePanel).dive().dive();

const mainDiv = wrapper.find('#detail-panel-warning-00000000-0000-0000-0000-000000000000').prop('text');
Expand Down Expand Up @@ -202,7 +211,7 @@ describe('When custom props are used', () => {
});

it('sets aria-hidden to true on main content div when isOpen and isFullscreen props are both true', () => {
const slidePanel = <SlidePanel isOpen isFullscreen />;
const slidePanel = <SlidePanel mainContent={<p>Main Content</p>} isOpen isFullscreen />;
const wrapper = enzymeIntl.shallowWithIntl(slidePanel).dive();

const mainDiv = wrapper.find('.main');
Expand Down
Loading
Loading