Skip to content

Commit

Permalink
♿ [bento][social-share] Enable focus highlighting on bento version of…
Browse files Browse the repository at this point in the history
… component (#31994)

* Social share focus POC

* add delegate focus and other review comments

* Add immportant to styles

* Add tests and documentation for focus feature

* Update tests to remove flakiness

* Add check for delegates focus

* Update description for class based css rule-set

* Update docs and remove overly specific description
  • Loading branch information
krdwan committed Jan 26, 2021
1 parent e172d78 commit 62803a2
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 1 deletion.
10 changes: 10 additions & 0 deletions extensions/amp-social-share/1.0/amp-social-share.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ amp-social-share:not(.i-amphtml-built) > :not([placeholder]) {
* See https://github.com/ampproject/amphtml/issues/4277 for more details.
*/

amp-social-share:focus {
outline: #0389ff solid 2px;
outline-offset: 2px;
}

amp-social-share::part(button):focus {
outline: none !important;
outline-offset: 0 !important;
}

/* Twitter Styling */
.amp-social-share-twitter {
color: #fff;
Expand Down
3 changes: 3 additions & 0 deletions extensions/amp-social-share/1.0/amp-social-share.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ AmpSocialShare['layoutSizeDefined'] = true;
/** @override */
AmpSocialShare['passthroughNonEmpty'] = true;

/** @override */
AmpSocialShare['delegatesFocus'] = true;

/** @override */
AmpSocialShare['props'] = {
'tabIndex': {attr: 'tabindex'},
Expand Down
4 changes: 4 additions & 0 deletions extensions/amp-social-share/1.0/social-share.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {dict} from '../../../src/utils/object';
import {getSocialConfig} from './social-share-config';
import {openWindowDialog} from '../../../src/dom';
import {useResourcesNotify} from '../../../src/preact/utils';
import {useStyles} from './social-share.jss';

const NAME = 'SocialShare';
const DEFAULT_WIDTH = 60;
Expand All @@ -50,6 +51,7 @@ export function SocialShare({
...rest
}) {
useResourcesNotify();
const classes = useStyles();
const checkPropsReturnValue = checkProps(
type,
endpoint,
Expand Down Expand Up @@ -83,6 +85,8 @@ export function SocialShare({
height: checkedHeight,
...style,
}}
part="button"
wrapperClassName={classes.button}
>
{processChildren(
/** @type {string} */ (type),
Expand Down
32 changes: 32 additions & 0 deletions extensions/amp-social-share/1.0/social-share.jss.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Copyright 2021 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {createUseStyles} from 'react-jss';

const button = {
'&:focus': {
outline: '#0389ff solid 2px',
outlineOffset: '2px',
},
};

const JSS = {
button,
};

// useStyles gets replaced for AMP builds via `babel-plugin-transform-jss`.
// eslint-disable-next-line local/no-export-side-effect
export const useStyles = createUseStyles(JSS);
33 changes: 33 additions & 0 deletions extensions/amp-social-share/1.0/test/test-amp-social-share.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,39 @@ describes.realWin(
).to.be.equal('inherit');
});

it('should focus on the host when an element in the shadow DOM receives focus', async () => {
element = win.document.createElement('amp-social-share');
element.setAttribute('type', 'email');
win.document.body.appendChild(element);
await waitForRender();

// element is not focused and does not have focus indication styles
expect(win.document.activeElement).to.not.equal(element);

// focus the button within the shadow DOM
const button = element.shadowRoot.querySelector("[part='button']");
button.focus();

// host receives focus
expect(win.document.activeElement).to.equal(element);
});

it('should allow focus directly on the host', async () => {
element = win.document.createElement('amp-social-share');
element.setAttribute('type', 'email');
win.document.body.appendChild(element);
await waitForRender();

// element is not focused and does not have focus indication styles
expect(win.document.activeElement).to.not.equal(element);

// focus the host
element.focus();

// host receives focus
expect(win.document.activeElement).to.equal(element);
});

describe('dynamically update attributes', () => {
it('updates default url and css class when "type" attribute is updated', async () => {
element = win.document.createElement('amp-social-share');
Expand Down
8 changes: 8 additions & 0 deletions extensions/amp-social-share/1.0/test/test-social-share.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,12 @@ describes.sandboxed('SocialShare 1.0 preact component', {}, () => {
);
}
);

it('should include the button class for focus styling', () => {
const jsx = <SocialShare {...dict({'type': 'email'})} />;
const wrapper = mount(jsx);

const button = wrapper.getDOMNode();
expect(button.className.includes('button')).to.be.true;
});
});
28 changes: 28 additions & 0 deletions extensions/amp-social-share/amp-social-share.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,34 @@ When customizing the style of an `amp-social-share` icon please ensure that the

## Accessibility

### Indication of focus

The `amp-social-share` element defaults to a blue outline as a visible focus indicator. It also defaults `tabindex=0` making it easy for a user to follow along as he or she tabs through multiple `amp-social-share` elements used together on a page.

The default focus indicator is achieved with the following CSS rule-set.

```css
amp-social-share:focus {
outline: #0389ff solid 2px;
outline-offset: 2px;
}
```

The default focus indicator can be overwritten by defining CSS styles for focus and including them within a `style` tag on an AMP HTML page. In the example below, the first CSS rule-set removes the focus indicator on all `amp-social-share` elements by setting the `outline` property to `none`. The second rule-set specifies a red outline (instead of the default blue) and also sets the `outline-offset` to be `3px` for all `amp-social-share` elements with the class `custom-focus`.

```css
amp-social-share:focus{
outline: none;
}

amp-social-share.custom-focus:focus {
outline: red solid 2px;
outline-offset: 3px;
}
```

With these CSS rules, `amp-social-share` elements would not show the visible focus indicator unless they included the class `custom-focus` in which case they would have the red outlined indicator.

### Color contrast

Note that `amp-social-share` with a `type` value of `twitter`, `whatsapp`, or `line` will display a button with a foreground/background color combination that falls below the 3:1 threshold recommended for non-text content defined in [WCAG 2.1 SC 1.4.11 Non-text Contrast](https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast.html).
Expand Down
13 changes: 12 additions & 1 deletion src/preact/base-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,10 @@ export class PreactBaseElement extends AMP.BaseElement {
this.hydrationPending_ = true;
} else {
// Create new shadow root.
shadowRoot = this.element.attachShadow({mode: 'open'});
shadowRoot = this.element.attachShadow({
mode: 'open',
delegatesFocus: Ctor['delegatesFocus'],
});

// The pre-constructed shadow root is required to have the stylesheet
// inline. Thus, only the new shadow roots share the stylesheets.
Expand Down Expand Up @@ -862,6 +865,14 @@ PreactBaseElement['shadowCss'] = null;
*/
PreactBaseElement['detached'] = false;

/**
* This enables the 'delegatesFocus' option when creating the shadow DOM for
* this component. A key feature of 'delegatesFocus' set to true is that
* when elements within the shadow DOM gain focus, the focus is also applied
* to the host element.
*/
PreactBaseElement['delegatesFocus'] = false;

/**
* Provides a mapping of Preact prop to AmpElement DOM attributes.
*
Expand Down
32 changes: 32 additions & 0 deletions test/unit/preact/test-base-element-mapping.js
Original file line number Diff line number Diff line change
Expand Up @@ -964,4 +964,36 @@ describes.realWin('PreactBaseElement', spec, (env) => {
expect(component).to.be.calledTwice;
});
});

describe('delegatesFocus mapping', () => {
let element;

beforeEach(async () => {
Impl['delegatesFocus'] = true;
Impl['passthroughNonEmpty'] = true;
element = html`
<amp-preact layout="fixed" width="100" height="100"></amp-preact>
`;
doc.body.appendChild(element);
await element.build();
await waitFor(() => component.callCount > 0, 'component rendered');
});

it('should focus on the host when an element in the shadow DOM receives focus', async () => {
// expect the shadowRoot to have delegatesFocus property set to true
expect(element.shadowRoot.delegatesFocus).to.be.true;

// initial focus is not on host
expect(doc.activeElement).to.not.equal(element);

// focus an element within the shadow DOM
const inner = element.shadowRoot.querySelector('#component');
// required to receive focus
inner.setAttribute('tabIndex', 0);
inner.focus();

// host receives focus and custom style for outline
expect(doc.activeElement).to.equal(element);
});
});
});

0 comments on commit 62803a2

Please sign in to comment.