Skip to content
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

fix(#145): ManifestSummary/Tooltip/Popover accessibility #146

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
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
7 changes: 5 additions & 2 deletions examples/minimal-ts-vite/examples/web-components/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@
interactive
style="float: right; position: relative; top: 40px; left: -10px"
>
<cai-indicator slot="trigger"></cai-indicator>
<cai-indicator
slot="trigger"
aria-label="Content Credentials for test image"
></cai-indicator>
<cai-manifest-summary
locale="en-US"
slot="content"
></cai-manifest-summary>
</cai-popover>
<img width="600" src="./test.jpg" />
<img width="600" src="./test.jpg" alt="test image" />
</div>
</body>
</html>
7 changes: 5 additions & 2 deletions examples/react-ts-vite/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,14 @@ function WebComponents({
return (
<div className="web-components">
<div className="wrapper">
<img src={imageUrl} />
<img src={imageUrl} alt="test image" />
{manifestStore ? (
<div>
<cai-popover interactive class="theme-spectrum">
<cai-indicator slot="trigger"></cai-indicator>
<cai-indicator
slot="trigger"
aria-label="Content Credentials for test image"
></cai-indicator>
<cai-manifest-summary
ref={summaryRef}
slot="content"
Expand Down
60 changes: 48 additions & 12 deletions packages/c2pa-wc/src/components/Icon/Icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,51 +45,87 @@ export class Icon extends LitElement {
static readonly matchers = [
{
pattern: /photoshop/i,
icon: html`<cai-icon-photoshop></cai-icon-photoshop>`,
icon: html`<cai-icon-photoshop
role="img"
aria-label="Photoshop"
></cai-icon-photoshop>`,
},
{
pattern: /adobe\sstock/i,
icon: html`<cai-icon-adobe-stock></cai-icon-adobe-stock>`,
icon: html`<cai-icon-adobe-stock
role="img"
aria-label="Adobe Stock"
></cai-icon-adobe-stock>`,
},
{
pattern: /adobe/i,
icon: html`<cai-icon-adobe></cai-icon-adobe>`,
icon: html`<cai-icon-adobe
role="img"
aria-label="Adobe"
></cai-icon-adobe>`,
},
{
pattern: /behance\.net/i,
icon: html`<cai-icon-behance></cai-icon-behance>`,
icon: html`<cai-icon-behance
role="img"
aria-label="Behance"
></cai-icon-behance>`,
},
{
pattern: /facebook\.com/i,
icon: html`<cai-icon-facebook></cai-icon-facebook>`,
icon: html`<cai-icon-facebook
role="img"
aria-label="Facebook"
></cai-icon-facebook>`,
},
{
pattern: /instagram\.com/i,
icon: html`<cai-icon-instagram></cai-icon-instagram>`,
icon: html`<cai-icon-instagram
role="img"
aria-label="Instagram"
></cai-icon-instagram>`,
},
{
pattern: /truepic/i,
icon: html`<cai-icon-truepic></cai-icon-truepic>`,
icon: html`<cai-icon-truepic
role="img"
aria-label="TruePic"
></cai-icon-truepic>`,
},
{
pattern: /twitter\.com/i,
icon: html`<cai-icon-twitter></cai-icon-twitter>`,
icon: html`<cai-icon-twitter
role="img"
aria-label="Twitter"
></cai-icon-twitter>`,
},
{
pattern: /lightroom/i,
icon: html`<cai-icon-lightroom></cai-icon-lightroom>`,
icon: html`<cai-icon-lightroom
role="img"
aria-label="Lightroom"
></cai-icon-lightroom>`,
},
{
pattern: /solana/i,
icon: html`<cai-icon-solana></cai-icon-solana`,
icon: html`<cai-icon-solana
role="img"
aria-label="Solana"
></cai-icon-solana>`,
},
{
pattern: /ethereum/i,
icon: html`<cai-icon-ethereum></cai-icon-ethereum>`,
icon: html`<cai-icon-ethereum
role="img"
aria-label="Ethereum"
></cai-icon-ethereum>`,
},
{
pattern: /linkedin/i,
icon: html`<cai-icon-linkedin></cai-icon-linkedin>`,
icon: html`<cai-icon-linkedin
role="img"
aria-label="LinkedIn"
></cai-icon-linkedin>`,
},
];

Expand Down
6 changes: 6 additions & 0 deletions packages/c2pa-wc/src/components/Indicator/Indicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,17 @@ export class Indicator extends LitElement {
display: inline-block;
width: var(--cai-indicator-size, 24px);
height: var(--cai-indicator-size, 24px);
border-radius: 50% 50% 0 50%;
line-height: 0;
}
.icon {
--cai-icon-width: var(--cai-indicator-size, 24px);
--cai-icon-height: var(--cai-indicator-size, 24px);
}
:host:focus-visible {
outline-color: var(--cai-focus-ring-color, #1473e6);
outline-offset: 1px;
}
`,
];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,6 @@ export class ManifestSummary extends Configurable(
#view-more {
display: block;
transition: all 150ms ease-in-out;
background-color: transparent;
border-radius: 9999px;
border: 2px solid var(--cai-button-color);
padding: 8px 0;
Expand All @@ -137,6 +136,8 @@ export class ManifestSummary extends Configurable(
text-decoration: none;
width: 100%;
color: var(--cai-primary-color);
outline-offset: 3px;
outline-color: var(--cai-focus-ring-color, #1473e6);
background-color: var(--cai-button-color);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export class MinimumViableProvenance extends Configurable(
return html`
<div class="container">
<div class="heading">
<div class="heading-text">
<div class="heading-text" role="heading" aria-level="2">
${this.strings['minimum-viable-provenance.header']}
</div>
</div>
Expand Down
3 changes: 3 additions & 0 deletions packages/c2pa-wc/src/components/PanelSection/PanelSection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export class PanelSection extends LitElement {
@property({ type: String })
helpText: string | null = null;

@property({ type: String })
headingLevel: '1' | '2' | '3' | '4' | '5' | '6' = '3';

static get styles() {
return [
defaultStyles,
Expand Down
140 changes: 124 additions & 16 deletions packages/c2pa-wc/src/components/Popover/Popover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export class Popover extends LitElement {
interactive = false;

@property({ type: String })
trigger: string = 'mouseenter:mouseleave focus:blur';
trigger: string = 'mouseenter:mouseleave click';

@property({ type: Number })
zIndex = 10;
Expand All @@ -101,6 +101,18 @@ export class Popover extends LitElement {
@query('#trigger')
triggerElement: HTMLElement | undefined;

private _triggerElementSlot: HTMLSlotElement | undefined;

private _triggerSlotAssignedNodes: Node[] = [];

private _triggerElementButton: HTMLElement | undefined;

private _contentElementSlot: HTMLSlotElement | undefined;

private _contentSlotAssignedNodes: Node[] = [];

private _hasTooltipRole = false;

// @TODO: respect updated properties
protected updated(
_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>,
Expand Down Expand Up @@ -196,10 +208,59 @@ export class Popover extends LitElement {
private _showTooltip() {
this._isShown = true;
this._updatePosition();
this.hostElement!.ownerDocument!.addEventListener(
'keydown',
this._onKeyDownEsc.bind(this),
);
if (!this._hasTooltipRole) {
this._triggerElementButton?.setAttribute('aria-expanded', 'true');
}
}

private _hideTooltip() {
this._isShown = false;
this.hostElement!.ownerDocument!.removeEventListener(
'keydown',
this._onKeyDownEsc.bind(this),
);
if (!this._hasTooltipRole) {
this._triggerElementButton?.setAttribute('aria-expanded', 'false');
}
}

private _toogleTooltip() {
if (!this._isShown) {
this._showTooltip();
} else {
this._hideTooltip();
}
}

private _onKeyDownEsc(e: KeyboardEvent) {
switch (e.key) {
case 'Escape':
if (this._isShown) {
e.stopPropagation();
e.preventDefault();
const restoreFocus = this.contains(document.activeElement);
this._hideTooltip();
if (restoreFocus) {
this._triggerElementButton!.focus();
}
}
break;
}
}

private _onKeyDownTrigger(e: KeyboardEvent) {
switch (e.key) {
case 'Enter':
case ' ':
e.stopPropagation();
e.preventDefault();
(e.target as HTMLElement).click();
break;
}
}

private _cleanupTriggers() {
Expand All @@ -212,27 +273,51 @@ export class Popover extends LitElement {
private _setTriggers() {
this._cleanupTriggers();
const triggers = this.trigger.split(/\s+/);
const toggleTooltipFn = this._toogleTooltip.bind(this);
const showTooltipFn = this._showTooltip.bind(this);
const hideTooltipFn = this._hideTooltip.bind(this);
const keydownTriggerFn = this._onKeyDownTrigger.bind(this);

this._eventCleanupFns = triggers.map((trigger) => {
const [show, hide] = trigger.split(':');
this.triggerElement!.addEventListener(show, this._showTooltip.bind(this));
if (this.interactive && hide === 'mouseleave') {
this.hostElement!.addEventListener(hide, this._hideTooltip.bind(this));
if (show === 'click') {
this.triggerElement!.addEventListener(show, toggleTooltipFn);
this.triggerElement!.addEventListener('keydown', keydownTriggerFn);
} else {
this.triggerElement!.addEventListener(
hide,
this._hideTooltip.bind(this),
show,
showTooltipFn,
show === 'focus',
);
}
return () => {
this.triggerElement!.removeEventListener(show, this._showTooltip);
if (this.interactive && hide === 'mouseleave') {
this.contentElement!.addEventListener(
this.hostElement!.addEventListener(hide, hideTooltipFn);
} else {
this.triggerElement!.addEventListener(
hide,
this._hideTooltip.bind(this),
hideTooltipFn,
hide === 'blur',
);
}
}
return () => {
if (show === 'click') {
this.triggerElement!.removeEventListener(show, toggleTooltipFn);
this.triggerElement!.removeEventListener('keydown', keydownTriggerFn);
} else {
this.triggerElement!.removeEventListener(hide, this._hideTooltip);
this.triggerElement!.removeEventListener(
show,
showTooltipFn,
show === 'focus',
);
if (this.interactive && hide === 'mouseleave') {
this.contentElement!.addEventListener(hide, hideTooltipFn);
} else {
this.triggerElement!.removeEventListener(
hide,
hideTooltipFn,
hide === 'blur',
);
}
}
};
});
Expand Down Expand Up @@ -315,6 +400,29 @@ export class Popover extends LitElement {
);

this.contentElement?.classList.add('hidden');

this._contentElementSlot = this.contentElement?.querySelector(
'slot[name="content"]',
) as HTMLSlotElement;
this._contentSlotAssignedNodes =
this._contentElementSlot?.assignedElements({ flatten: true }) ?? [];
this._hasTooltipRole = this._contentSlotAssignedNodes.some(
(node) =>
node instanceof HTMLElement && node.getAttribute('role') === 'tooltip',
);

this._triggerElementSlot = this.triggerElement?.querySelector(
'slot[name="trigger"]',
) as HTMLSlotElement;
this._triggerSlotAssignedNodes =
this._triggerElementSlot?.assignedElements({ flatten: true }) ?? [];
this._triggerElementButton = this
._triggerSlotAssignedNodes[0] as HTMLElement;
this._triggerElementButton.setAttribute('role', 'button');
this._triggerElementButton.setAttribute('tabindex', '0');
if (!this._hasTooltipRole) {
this._triggerElementButton.setAttribute('aria-expanded', 'false');
}
}

disconnectedCallback(): void {
Expand All @@ -333,6 +441,10 @@ export class Popover extends LitElement {
};

return html`<div id="element">
<div id="trigger">
<div class="hidden-layer"></div>
<slot name="trigger"></slot>
</div>
<div
id="content"
class=${classMap(contentClassMap)}
Expand All @@ -356,10 +468,6 @@ export class Popover extends LitElement {
<slot name="content"></slot>
${this.arrow ? html`<div id="arrow"></div>` : null}
</div>
<div id="trigger">
<div class="hidden-layer"></div>
<slot name="trigger"></slot>
</div>
</div>`;
}
}
Loading