Skip to content

Commit c24f233

Browse files
authored
Merge pull request #555 from gselderslaghs/cards-accessibility
accessibility(Cards) refactored component based, implemented tab index and aria expanded
2 parents e4c6796 + 9f45985 commit c24f233

File tree

3 files changed

+189
-50
lines changed

3 files changed

+189
-50
lines changed

sass/components/_cards.scss

+18-6
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@
1919
.card-title {
2020
font-size: 24px;
2121
font-weight: 300;
22-
&.activator {
23-
cursor: pointer;
24-
}
2522
}
2623

2724
// Card Sizes
@@ -32,13 +29,16 @@
3229
max-height: 60%;
3330
overflow: hidden;
3431
}
32+
3533
.card-image + .card-content {
3634
max-height: 40%;
3735
}
36+
3837
.card-content {
3938
max-height: 100%;
4039
overflow: hidden;
4140
}
41+
4242
.card-action {
4343
position: absolute;
4444
bottom: 0;
@@ -77,6 +77,7 @@
7777

7878
.card-image {
7979
max-width: 50%;
80+
8081
img {
8182
border-radius: 2px 0 0 2px;
8283
max-width: 100%;
@@ -108,9 +109,6 @@
108109
}
109110
}
110111

111-
112-
113-
114112
.card-image {
115113
position: relative;
116114

@@ -134,6 +132,15 @@
134132
max-width: 100%;
135133
padding: 24px;
136134
}
135+
136+
.activator {
137+
position: absolute;
138+
left: 0;
139+
right: 0;
140+
top:0;
141+
bottom: 0;
142+
cursor: pointer;
143+
}
137144
}
138145

139146
.card-content {
@@ -143,6 +150,7 @@
143150
p {
144151
margin: 0;
145152
}
153+
146154
.card-title {
147155
display: block;
148156
line-height: 32px;
@@ -151,6 +159,10 @@
151159
i {
152160
line-height: 32px;
153161
}
162+
163+
&.activator {
164+
cursor: pointer;
165+
}
154166
}
155167
}
156168

src/cards.ts

+167-42
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,170 @@
1-
export class Cards {
2-
3-
static Init() {
4-
if (typeof document !== 'undefined') document.addEventListener("DOMContentLoaded", () => {
5-
document.body.addEventListener('click', e => {
6-
const trigger = <HTMLElement>e.target;
7-
8-
const card: HTMLElement = trigger.closest('.card');
9-
if (!card) return;
10-
11-
const cardReveal = <HTMLElement|null>Array.from(card.children).find(elem => elem.classList.contains('card-reveal'));
12-
if (!cardReveal) return;
13-
const initialOverflow = getComputedStyle(card).overflow;
14-
15-
// Close Card
16-
const closeArea = cardReveal.querySelector('.card-reveal .card-title');
17-
if (trigger === closeArea || closeArea.contains(trigger)) {
18-
const duration = 225;
19-
cardReveal.style.transition = `transform ${duration}ms ease`; //easeInOutQuad
20-
cardReveal.style.transform = 'translateY(0)';
21-
setTimeout(() => {
22-
cardReveal.style.display = 'none';
23-
card.style.overflow = initialOverflow;
24-
}, duration);
25-
};
26-
27-
// Reveal Card
28-
const activators = card.querySelectorAll('.activator');
29-
activators.forEach(activator => {
30-
if (trigger === activator || activator.contains(trigger)) {
31-
card.style.overflow = 'hidden';
32-
cardReveal.style.display = 'block';
33-
setTimeout(() => {
34-
const duration = 300;
35-
cardReveal.style.transition = `transform ${duration}ms ease`; //easeInOutQuad
36-
cardReveal.style.transform = 'translateY(-100%)';
37-
}, 1);
38-
}
39-
});
40-
41-
});
42-
});
1+
import { Utils } from './utils';
2+
import { Component, BaseOptions, InitElements, MElement, Openable } from './component';
3+
4+
export interface CardsOptions extends BaseOptions {
5+
onOpen: (el: Element) => void;
6+
onClose: (el: Element) => void;
7+
inDuration: number;
8+
outDuration: number;
9+
}
10+
11+
const _defaults: CardsOptions = {
12+
onOpen: null,
13+
onClose: null,
14+
inDuration: 225,
15+
outDuration: 300
16+
};
17+
18+
export class Cards extends Component<CardsOptions> implements Openable {
19+
isOpen: boolean = false;
20+
private readonly cardReveal: HTMLElement | null;
21+
private readonly initialOverflow: string;
22+
private _activators: HTMLElement[] | null;
23+
private cardRevealClose: HTMLElement | null;
24+
25+
constructor(el: HTMLElement, options: Partial<CardsOptions>) {
26+
super(el, options, Cards);
27+
(this.el as any).M_Cards = this;
28+
29+
this.options = {
30+
...Cards.defaults,
31+
...options
32+
};
33+
34+
this.cardReveal = <HTMLElement | null>Array.from(this.el.children).find(elem => elem.classList.contains('card-reveal'));
35+
36+
if (this.cardReveal) {
37+
this.initialOverflow = getComputedStyle(this.el).overflow;
38+
this._activators = Array.from(this.el.querySelectorAll('.activator'));
39+
this._activators.forEach((el: HTMLElement) => el.tabIndex = 0);
40+
this.cardRevealClose = this.cardReveal.querySelector('.card-reveal .card-title .close');
41+
this.cardRevealClose.tabIndex = -1;
42+
this.cardReveal.ariaExpanded = 'false';
43+
this._setupEventHandlers();
44+
}
45+
}
46+
47+
static get defaults(): CardsOptions {
48+
return _defaults;
49+
}
50+
51+
/**
52+
* Initializes instance of Cards.
53+
* @param el HTML element.
54+
* @param options Component options.
55+
*/
56+
static init(el: HTMLElement, options?: Partial<CardsOptions>): Cards;
57+
/**
58+
* Initializes instances of Cards.
59+
* @param els HTML elements.
60+
* @param options Component options.
61+
*/
62+
static init(els: InitElements<MElement>, options?: Partial<CardsOptions>): Cards[];
63+
/**
64+
* Initializes instances of Cards.
65+
* @param els HTML elements.
66+
* @param options Component options.
67+
*/
68+
static init(els: HTMLElement | InitElements<MElement>, options?: Partial<CardsOptions>): Cards | Cards[] {
69+
return super.init(els, options, Cards);
70+
}
4371

72+
static getInstance(el: HTMLElement): Cards {
73+
return (el as any).M_Cards;
4474
}
75+
76+
/**
77+
* {@inheritDoc}
78+
*/
79+
destroy() {
80+
this._removeEventHandlers();
81+
this._activators = [];
82+
}
83+
84+
_setupEventHandlers = () => {
85+
this._activators.forEach((el: HTMLElement) => {
86+
el.addEventListener('click', this._handleClickInteraction);
87+
el.addEventListener('keypress', this._handleKeypressEvent);
88+
});
89+
};
90+
91+
_removeEventHandlers = () => {
92+
this._activators.forEach((el: HTMLElement) => {
93+
el.removeEventListener('click', this._handleClickInteraction);
94+
el.removeEventListener('keypress', this._handleKeypressEvent);
95+
});
96+
};
97+
98+
_handleClickInteraction = () => {
99+
this._handleRevealEvent();
100+
};
101+
102+
_handleKeypressEvent: (e: KeyboardEvent) => void = (e: KeyboardEvent) => {
103+
if (Utils.keys.ENTER.includes(e.key)) {
104+
this._handleRevealEvent();
105+
}
106+
};
107+
108+
_handleRevealEvent = () => {
109+
// Reveal Card
110+
this._activators.forEach((el: HTMLElement) => el.tabIndex = -1);
111+
this.open();
112+
};
113+
114+
_setupRevealCloseEventHandlers = () => {
115+
this.cardRevealClose.addEventListener('click', this.close);
116+
this.cardRevealClose.addEventListener('keypress', this._handleKeypressCloseEvent);
117+
};
118+
119+
_removeRevealCloseEventHandlers = () => {
120+
this.cardRevealClose.addEventListener('click', this.close);
121+
this.cardRevealClose.addEventListener('keypress', this._handleKeypressCloseEvent);
122+
};
123+
124+
_handleKeypressCloseEvent: (e: KeyboardEvent) => void = (e: KeyboardEvent) => {
125+
if (Utils.keys.ENTER.includes(e.key)) {
126+
this.close();
127+
}
128+
};
129+
130+
/**
131+
* Show card reveal.
132+
*/
133+
open: () => void = () => {
134+
if (this.isOpen) return;
135+
this.isOpen = true;
136+
this.el.style.overflow = 'hidden';
137+
this.cardReveal.style.display = 'block';
138+
this.cardReveal.ariaExpanded = 'true';
139+
this.cardRevealClose.tabIndex = 0;
140+
setTimeout(() => {
141+
this.cardReveal.style.transition = `transform ${this.options.outDuration}ms ease`; //easeInOutQuad
142+
this.cardReveal.style.transform = 'translateY(-100%)';
143+
}, 1);
144+
if (typeof this.options.onOpen === 'function') {
145+
this.options.onOpen.call(this);
146+
}
147+
this._setupRevealCloseEventHandlers();
148+
};
149+
150+
/**
151+
* Hide card reveal.
152+
*/
153+
close: () => void = () => {
154+
if (!this.isOpen) return;
155+
this.isOpen = false;
156+
this.cardReveal.style.transition = `transform ${this.options.inDuration}ms ease`; //easeInOutQuad
157+
this.cardReveal.style.transform = 'translateY(0)';
158+
setTimeout(() => {
159+
this.cardReveal.style.display = 'none';
160+
this.cardReveal.ariaExpanded = 'false';
161+
this._activators.forEach((el: HTMLElement) => el.tabIndex = 0);
162+
this.cardRevealClose.tabIndex = -1;
163+
this.el.style.overflow = this.initialOverflow;
164+
}, this.options.inDuration);
165+
if (typeof this.options.onClose === 'function') {
166+
this.options.onClose.call(this);
167+
}
168+
this._removeRevealCloseEventHandlers();
169+
};
45170
}

src/index.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Autocomplete, AutocompleteOptions } from './autocomplete';
22
import { FloatingActionButton, FloatingActionButtonOptions } from './buttons';
3-
import { Cards } from './cards';
3+
import { Cards, CardsOptions } from './cards';
44
import { Carousel, CarouselOptions } from './carousel';
55
import { CharacterCounter, CharacterCounterOptions } from './characterCounter';
66
import { Chips, ChipsOptions } from './chips';
@@ -64,6 +64,7 @@ export function Button(children: any = '') {
6464

6565
export interface AutoInitOptions {
6666
Autocomplete?: Partial<AutocompleteOptions>
67+
Cards?: Partial<CardsOptions>
6768
Carousel?: Partial<CarouselOptions>
6869
Chips?: Partial<ChipsOptions>
6970
Collapsible?: Partial<CollapsibleOptions>
@@ -91,6 +92,7 @@ export interface AutoInitOptions {
9192
export function AutoInit(context: HTMLElement = document.body, options?: Partial<AutoInitOptions>) {
9293
let registry = {
9394
Autocomplete: context.querySelectorAll('.autocomplete:not(.no-autoinit)'),
95+
Cards: context.querySelectorAll('.cards:not(.no-autoinit)'),
9496
Carousel: context.querySelectorAll('.carousel:not(.no-autoinit)'),
9597
Chips: context.querySelectorAll('.chips:not(.no-autoinit)'),
9698
Collapsible: context.querySelectorAll('.collapsible:not(.no-autoinit)'),
@@ -110,6 +112,7 @@ export function AutoInit(context: HTMLElement = document.body, options?: Partial
110112
FloatingActionButton: context.querySelectorAll('.fixed-action-btn:not(.no-autoinit)')
111113
};
112114
Autocomplete.init(registry.Autocomplete, options?.Autocomplete ?? {});
115+
Cards.init(registry.Cards, options?.Cards ?? {})
113116
Carousel.init(registry.Carousel, options?.Carousel ?? {});
114117
Chips.init(registry.Chips, options?.Chips ?? {});
115118
Collapsible.init(registry.Collapsible, options?.Collapsible ?? {});
@@ -137,7 +140,6 @@ if (typeof document !== 'undefined') {
137140
document.addEventListener('focus', Utils.docHandleFocus, true);
138141
document.addEventListener('blur', Utils.docHandleBlur, true);
139142
}
140-
Cards.Init();
141143
Forms.Init();
142144
Chips.Init();
143145
Waves.Init();

0 commit comments

Comments
 (0)