diff --git a/elements/pfe-primary-detail/README.md b/elements/pfe-primary-detail/README.md
index 65546522c7..e2d38c76fe 100755
--- a/elements/pfe-primary-detail/README.md
+++ b/elements/pfe-primary-detail/README.md
@@ -1,7 +1,7 @@
# PatternFly Element | Primary detail element
-
## Usage
+
A primary-detail layout is an interface that shows a list of items and the corresponding details of the selected item.
This component is an implementation of one of the "Primary detail simple list in card" from [Patternfly React](https://www.patternfly.org/v4/demos/primary-detail), more layouts may be implemented later.
@@ -56,8 +56,30 @@ This component is an implementation of one of the "Primary detail simple list in
```
### Accessibility
+
The default markup should have semantic markup if the component can't load, once it loads the component the appropriate tab interactions and appropriate markup for assistive tech is handled for you.
+#### Focus Indicator Styles
+
+The component requires visible focus indicator styles for focusable elements (ie.`links`, `buttons`, `[tabindex="0"]`) in order to meet [**WCAG 2.0/2.1 AA compliance**](https://www.w3.org/WAI/WCAG21/quickref/#focus-visible). Below is a good example of styles to use for the focus indicator, these styles match the focus indicator of `pfe-navigation`. The `padding` style is to increase the clickable area of links in order to help users have a better experience when trying to click the links, this also helps users with limited mobility.
+
+```html
+ pfe-primary-detail .focus-styles:focus,
+ pfe-primary-detail .focus-styles:hover {
+ outline: 1px dashed #000;
+ outline-width: 2px;
+ }
+
+ pfe-primary-detail ul.focus-styles:hover,
+ pfe-primary-detail :not(pfe-cta).focus-styles:hover {
+ outline: 0;
+ }
+
+ pfe-primary-detail a.focus-styles {
+ padding: 8px;
+ }
+```
+
## Slots
For this component to work, there should be an equal number of `details-nav` and `details` slotted elements.
diff --git a/elements/pfe-primary-detail/demo/index.html b/elements/pfe-primary-detail/demo/index.html
index 93cd917330..1f8d49f8f3 100755
--- a/elements/pfe-primary-detail/demo/index.html
+++ b/elements/pfe-primary-detail/demo/index.html
@@ -69,15 +69,80 @@
.ansible-styles .pfe-primary-detail__toggle--active {
font-weight: bold;
}
+
+ pfe-primary-detail .focus-styles:focus,
+ pfe-primary-detail .focus-styles:hover {
+ outline: 1px dashed #000;
+ outline-width: 2px;
+ }
+
+ pfe-primary-detail ul.focus-styles:hover,
+ pfe-primary-detail :not(pfe-cta).focus-styles:hover {
+ outline: 0;
+ }
+
+ pfe-primary-detail a.focus-styles {
+ padding: 8px;
+ }
pfe-primary-detail
+ Default styles with Focus Indicator styles
+
+
+ Infrastructure and Management
+
+
+ Cloud Computing
+
+
+ Storage
+
+
+ Runtimes
+
+
+
Default styles with a nav footer
-
+
Infrastructure and Management
+
- Lorum ipsum dolor sit amet
- Aliquam tincidunt mauris eu risus
diff --git a/elements/pfe-primary-detail/src/pfe-primary-detail.html b/elements/pfe-primary-detail/src/pfe-primary-detail.html
index d9671105c5..d904f82497 100755
--- a/elements/pfe-primary-detail/src/pfe-primary-detail.html
+++ b/elements/pfe-primary-detail/src/pfe-primary-detail.html
@@ -1,8 +1,8 @@
-
+
diff --git a/elements/pfe-primary-detail/src/pfe-primary-detail.js b/elements/pfe-primary-detail/src/pfe-primary-detail.js
index cca246d5ef..a2952d7e69 100755
--- a/elements/pfe-primary-detail/src/pfe-primary-detail.js
+++ b/elements/pfe-primary-detail/src/pfe-primary-detail.js
@@ -10,7 +10,6 @@ const lightDomObserverConfig = {
childList: true
};
-// @todo Add keyboard controls for arrows?
// @todo Add functions to open a specific item by index or ID
class PfePrimaryDetail extends PFElement {
static get tag() {
@@ -93,6 +92,8 @@ class PfePrimaryDetail extends PFElement {
this._initDetailsNav = this._initDetailsNav.bind(this);
this._initDetail = this._initDetail.bind(this);
this._processLightDom = this._processLightDom.bind(this);
+ this._a11yKeyBoardControls = this._a11yKeyBoardControls.bind(this);
+ this._a11yFocusStyleHandler = this._a11yFocusStyleHandler.bind(this);
this._slots = {
detailsNav: null,
@@ -106,6 +107,9 @@ class PfePrimaryDetail extends PFElement {
this._detailsNav = this.shadowRoot.getElementById("details-nav");
this._detailsWrapper = this.shadowRoot.getElementById("details-wrapper");
+
+ // Store all focusable element types in variable
+ this._focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
}
connectedCallback() {
@@ -121,6 +125,12 @@ class PfePrimaryDetail extends PFElement {
// Set first item as active for initial load
this._handleHideShow({ target: this._slots.detailsNav[0] });
+
+ // A11y Features: add keydown event listener to activate keyboard controls
+ this.addEventListener("keydown", this._a11yKeyBoardControls);
+
+ // A11y Features: add class to all focusable elements for focus indicator styles
+ this._a11yFocusStyleHandler();
}
disconnectedCallback() {
@@ -131,6 +141,9 @@ class PfePrimaryDetail extends PFElement {
this._slots.detailsNav[index].removeEventListener("click", this._handleHideShow);
}
}
+
+ // Remove keydown event listener if component disconnects
+ this.removeEventListener("keydown", this._a11yKeyBoardControls);
}
/**
@@ -173,8 +186,17 @@ class PfePrimaryDetail extends PFElement {
toggle.setAttribute("role", "tab");
toggle.setAttribute("aria-selected", "false");
+ toggle.setAttribute("tabindex", "-1");
+ toggle.setAttribute("aria-hidden", "true");
+
+ // Add active tab state to tab that is active on page load
+ if (toggle.hasAttribute("aria-selected") && toggle.getAttribute("aria-selected") === "true") {
+ toggle.setAttribute("tabindex", "0");
+ toggle.setAttribute("aria-hidden", "false");
+ }
toggle.addEventListener("click", this._handleHideShow);
+
this._slots.detailsNav[index] = toggle;
detailNavElement.replaceWith(toggle);
}
@@ -203,8 +225,15 @@ class PfePrimaryDetail extends PFElement {
}
detail.setAttribute("role", "tabpanel");
+ detail.setAttribute("tabindex", "-1");
detail.setAttribute("aria-hidden", "true");
+ // Add active tab panel state to tab panel that is active on page load
+ if (detail.hasAttribute("aria-hidden") && detail.getAttribute("aria-hidden") === "false") {
+ detail.setAttribute("tabindex", "0");
+ detail.setAttribute("aria-hidden", "false");
+ }
+
const toggleId = this._slots.detailsNav[index].getAttribute("id");
if (!detail.hasAttribute("aria-labelledby") && toggleId) {
detail.setAttribute("aria-labelledby", toggleId);
@@ -247,7 +276,7 @@ class PfePrimaryDetail extends PFElement {
this._slots.details.forEach((detail, index) => {
this._initDetail(detail, index);
});
- }
+ } // end _processLightDom()
/**
* Handles changes in state
@@ -255,6 +284,7 @@ class PfePrimaryDetail extends PFElement {
*/
_handleHideShow(e) {
const nextToggle = e.target;
+ const key = e.key;
if (typeof nextToggle === "undefined") {
return;
@@ -276,8 +306,21 @@ class PfePrimaryDetail extends PFElement {
// Remove Current Item's active attributes
currentToggle.setAttribute("aria-selected", "false");
+ /**
+ A11y note:
+ tabindex = -1 removes element from the tab sequence, set when tab is not selected so that only the active tab (selected tab) is in the tab sequence, when HTML button is used for tab you do not need to set tabindex = 0 on the button when it is active so the attribute should just be removed when the button is active
+
+ when any other HTML element is used such as a heading you will need to explicitly add tabindex = 0
- // Remove Current Detail's attributes
+ @resource:
+ https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-2/tabs.html
+ */
+ // Set current toggle's attribute to inactive state
+ currentToggle.setAttribute("tabindex", "-1");
+ currentToggle.setAttribute("aria-hidden", "true");
+
+ // Set Current Detail's attributes to inactive state
+ currentDetails.setAttribute("tabindex", "-1");
currentDetails.setAttribute("aria-hidden", "true");
this.emitEvent(PfePrimaryDetail.events.hiddenTab, {
@@ -291,7 +334,19 @@ class PfePrimaryDetail extends PFElement {
// Add active attributes to Next Item
nextToggle.setAttribute("aria-selected", "true");
- // Add active attributes to Next Details
+ /**
+ A11y note:
+ tabindex = 0 ensures the tabpanel is in the tab sequence, helps AT move to panel content, helps ensure correct behaviour (especially when the tabpanel does NOT contain any focusable elements)
+
+ @resource:
+ https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-2/tabs.html
+ */
+ // Explicitly set tabindex 0 bc heading is being used instead of button to fix IE11 issues
+ nextToggle.setAttribute("tabindex", "0");
+ nextToggle.setAttribute("aria-hidden", "false");
+
+ // Add inactive attributes to Next Details
+ nextDetails.setAttribute("tabindex", "0");
nextDetails.setAttribute("aria-hidden", "false");
this.emitEvent(PfePrimaryDetail.events.shownTab, {
@@ -300,16 +355,189 @@ class PfePrimaryDetail extends PFElement {
details: nextDetails
}
});
+ } // end _handleHideShow()
- // Set focus to pane
- const firstFocusableElement = nextDetails.querySelector(
- 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
- );
- if (firstFocusableElement) {
- firstFocusableElement.focus();
- }
+ /**
+ * A11y features:
+ * Check if active element is a tab toggle
+ * @param {object} element Target slotted element
+ */
+ _isToggle(element) {
+ return element.getAttribute("slot") === "details-nav";
+ }
+
+ /**
+ * Check if active element is a tab panel
+ * @param {object} element Target slotted element
+ */
+ _isPanel(element) {
+ return element.getAttribute("slot") === "details";
+ }
+
+ /**
+ * Get all tab toggles
+ */
+ _getAllToggles() {
+ return this._slots.detailsNav;
}
-}
+
+ /**
+ * Get all tab panels as an array
+ */
+ _getAllPanels() {
+ return this._slots.details;
+ }
+
+ /**
+ * Get the corresponding active tab panel for the active tab toggle
+ */
+ _getActivePanel() {
+ const toggles = this._getAllToggles();
+ let newIndex = toggles.findIndex(toggle => toggle === document.activeElement);
+
+ return toggles[newIndex % toggles.length].nextElementSibling;
+ }
+
+ /**
+ * Get last item in active tab panel
+ */
+ _getLastItem() {
+ const panels = this._getAllPanels();
+ const activePanel = this._getActivePanel();
+ const activePanelChildren = [...activePanel.children];
+
+ return activePanelChildren[activePanelChildren.length - 1];
+ }
+
+ /**
+ * Get previous toggle in relation to the active toggle
+ */
+ _getPrevToggle() {
+ const toggles = this._getAllToggles();
+ let newIndex = toggles.findIndex(toggle => toggle === document.activeElement) - 1;
+
+ return toggles[(newIndex + toggles.length) % toggles.length];
+ }
+
+ /**
+ * Get currently active toggle
+ */
+ _getActiveToggle() {
+ const toggles = this._getAllToggles();
+ let newIndex = toggles.findIndex(toggle => toggle === document.activeElement);
+
+ return toggles[newIndex % toggles.length];
+ }
+
+ /**
+ * Get next toggle in relation to the active toggle
+ */
+ _getNextToggle() {
+ const toggles = this._getAllToggles();
+ let newIndex = toggles.findIndex(toggle => toggle === document.activeElement) + 1;
+
+ return toggles[newIndex % toggles.length];
+ }
+
+ /**
+ * Get first toggle in the toggle list
+ */
+ _getFirstToggle() {
+ const firstToggle = this._getAllToggles;
+
+ return firstToggle[0];
+ }
+
+ /**
+ * Get last toggle in the toggle list
+ */
+ _getLastToggle() {
+ const lastToggle = this._getAllToggles;
+
+ return lastToggle[lastToggle.length - 1];
+ }
+
+
+ /**
+ * Focus styles class
+ * Add class to focusable elements in order to style with the :focus/:hover psuedo selectors
+ */
+ _a11yFocusStyleHandler() {
+
+ const componentFocusableElements = this.querySelectorAll(this._focusableElements);
+
+ componentFocusableElements.forEach(element => {
+ element.classList.add("focus-styles");
+ });
+
+ }
+
+ /**
+ * Manual user activation vertical tab
+ * @param {event} Target event
+ */
+ _a11yKeyBoardControls(event) {
+ const currentElement = event.target;
+
+ if (!this._isToggle(currentElement)) {
+ return;
+ }
+
+ let newToggle;
+
+ switch (event.key) {
+ // case "Tab":
+ // Tab (Default tab behavior)
+ /// When focus moves into the tab list, places focus on the active tab element
+ /// When the focus is in the tab list, move focus away from active tab element to next element in tab order which is the tabpanel element
+ /// When focus is moved outside of the tab list focus moves to the next focusable item in the DOM order
+
+ case "ArrowUp":
+ case "Up":
+ case "ArrowLeft":
+ case "Left":
+ // Up Arrow/Left Arrow
+ /// When tab has focus:
+ /// Moves focus to the next tab
+ /// If focus is on the last tab, moves focus to the first tab
+
+ newToggle = this._getPrevToggle();
+ break;
+
+ case "ArrowDown":
+ case "Down":
+ case "ArrowRight":
+ case "Right":
+ // Down Arrow/Right Arrow
+ /// When tab has focus:
+ /// Moves focus to previous tab
+ /// If focus is on the first tab, moves to the last tab
+ /// Activates the newly focused tab
+
+ newToggle = this._getNextToggle();
+ break;
+
+ case "Home":
+ // Home
+ //// When a tab has focus, moves focus to the first tab
+
+ newToggle = this._getFirstToggle();
+ break;
+
+ case "End":
+ // End
+ /// When a tab has focus, moves focus to the last tab
+
+ newToggle = this._getLastToggle();
+ break;
+
+ default:
+ return;
+ }
+
+ newToggle.focus();
+ } // end _a11yKeyBoardControls()
+} // end Class
PFElement.create(PfePrimaryDetail);
diff --git a/elements/pfe-primary-detail/src/pfe-primary-detail.scss b/elements/pfe-primary-detail/src/pfe-primary-detail.scss
index 63f4a13f51..421e2dac1a 100755
--- a/elements/pfe-primary-detail/src/pfe-primary-detail.scss
+++ b/elements/pfe-primary-detail/src/pfe-primary-detail.scss
@@ -94,3 +94,28 @@ $LOCAL-VARIABLES: (
visibility: visible!important;
}
}
+
+::slotted([aria-hidden='true']) {
+ // show detail to visual users but temporarily hide detail from screen readers
+ display: block!important;
+}
+
+::slotted([slot="details-nav--footer"]) {
+ border-right: pfe-local(Border) !important;
+}
+
+// @todo: add note in readme with focus indicator example and show ppl that they need to add a good focus indicator to focusable elements
+::slotted(.focus-styles:focus),
+::slotted(.focus-styles:hover) {
+ outline: 1px dashed #000 !important;
+ outline-width: 2px !important;
+}
+
+::slotted(ul.focus-styles:hover) {
+ outline: 0 !important;
+}
+
+::slotted(a.focus-styles) {
+ // Increase clickable area of links
+ padding: 8px !important;
+}