Skip to content

Commit

Permalink
Merge pull request #555 from gselderslaghs/cards-accessibility
Browse files Browse the repository at this point in the history
accessibility(Cards) refactored component based, implemented tab index and aria expanded
  • Loading branch information
wuda-io authored Dec 21, 2024
2 parents e4c6796 + 9f45985 commit c24f233
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 50 deletions.
24 changes: 18 additions & 6 deletions sass/components/_cards.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@
.card-title {
font-size: 24px;
font-weight: 300;
&.activator {
cursor: pointer;
}
}

// Card Sizes
Expand All @@ -32,13 +29,16 @@
max-height: 60%;
overflow: hidden;
}

.card-image + .card-content {
max-height: 40%;
}

.card-content {
max-height: 100%;
overflow: hidden;
}

.card-action {
position: absolute;
bottom: 0;
Expand Down Expand Up @@ -77,6 +77,7 @@

.card-image {
max-width: 50%;

img {
border-radius: 2px 0 0 2px;
max-width: 100%;
Expand Down Expand Up @@ -108,9 +109,6 @@
}
}




.card-image {
position: relative;

Expand All @@ -134,6 +132,15 @@
max-width: 100%;
padding: 24px;
}

.activator {
position: absolute;
left: 0;
right: 0;
top:0;
bottom: 0;
cursor: pointer;
}
}

.card-content {
Expand All @@ -143,6 +150,7 @@
p {
margin: 0;
}

.card-title {
display: block;
line-height: 32px;
Expand All @@ -151,6 +159,10 @@
i {
line-height: 32px;
}

&.activator {
cursor: pointer;
}
}
}

Expand Down
209 changes: 167 additions & 42 deletions src/cards.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,170 @@
export class Cards {

static Init() {
if (typeof document !== 'undefined') document.addEventListener("DOMContentLoaded", () => {
document.body.addEventListener('click', e => {
const trigger = <HTMLElement>e.target;

const card: HTMLElement = trigger.closest('.card');
if (!card) return;

const cardReveal = <HTMLElement|null>Array.from(card.children).find(elem => elem.classList.contains('card-reveal'));
if (!cardReveal) return;
const initialOverflow = getComputedStyle(card).overflow;

// Close Card
const closeArea = cardReveal.querySelector('.card-reveal .card-title');
if (trigger === closeArea || closeArea.contains(trigger)) {
const duration = 225;
cardReveal.style.transition = `transform ${duration}ms ease`; //easeInOutQuad
cardReveal.style.transform = 'translateY(0)';
setTimeout(() => {
cardReveal.style.display = 'none';
card.style.overflow = initialOverflow;
}, duration);
};

// Reveal Card
const activators = card.querySelectorAll('.activator');
activators.forEach(activator => {
if (trigger === activator || activator.contains(trigger)) {
card.style.overflow = 'hidden';
cardReveal.style.display = 'block';
setTimeout(() => {
const duration = 300;
cardReveal.style.transition = `transform ${duration}ms ease`; //easeInOutQuad
cardReveal.style.transform = 'translateY(-100%)';
}, 1);
}
});

});
});
import { Utils } from './utils';
import { Component, BaseOptions, InitElements, MElement, Openable } from './component';

export interface CardsOptions extends BaseOptions {
onOpen: (el: Element) => void;
onClose: (el: Element) => void;
inDuration: number;
outDuration: number;
}

const _defaults: CardsOptions = {
onOpen: null,
onClose: null,
inDuration: 225,
outDuration: 300
};

export class Cards extends Component<CardsOptions> implements Openable {
isOpen: boolean = false;
private readonly cardReveal: HTMLElement | null;
private readonly initialOverflow: string;
private _activators: HTMLElement[] | null;
private cardRevealClose: HTMLElement | null;

constructor(el: HTMLElement, options: Partial<CardsOptions>) {
super(el, options, Cards);
(this.el as any).M_Cards = this;

this.options = {
...Cards.defaults,
...options
};

this.cardReveal = <HTMLElement | null>Array.from(this.el.children).find(elem => elem.classList.contains('card-reveal'));

if (this.cardReveal) {
this.initialOverflow = getComputedStyle(this.el).overflow;
this._activators = Array.from(this.el.querySelectorAll('.activator'));
this._activators.forEach((el: HTMLElement) => el.tabIndex = 0);
this.cardRevealClose = this.cardReveal.querySelector('.card-reveal .card-title .close');
this.cardRevealClose.tabIndex = -1;
this.cardReveal.ariaExpanded = 'false';
this._setupEventHandlers();
}
}

static get defaults(): CardsOptions {
return _defaults;
}

/**
* Initializes instance of Cards.
* @param el HTML element.
* @param options Component options.
*/
static init(el: HTMLElement, options?: Partial<CardsOptions>): Cards;
/**
* Initializes instances of Cards.
* @param els HTML elements.
* @param options Component options.
*/
static init(els: InitElements<MElement>, options?: Partial<CardsOptions>): Cards[];
/**
* Initializes instances of Cards.
* @param els HTML elements.
* @param options Component options.
*/
static init(els: HTMLElement | InitElements<MElement>, options?: Partial<CardsOptions>): Cards | Cards[] {
return super.init(els, options, Cards);
}

static getInstance(el: HTMLElement): Cards {
return (el as any).M_Cards;
}

/**
* {@inheritDoc}
*/
destroy() {
this._removeEventHandlers();
this._activators = [];
}

_setupEventHandlers = () => {
this._activators.forEach((el: HTMLElement) => {
el.addEventListener('click', this._handleClickInteraction);
el.addEventListener('keypress', this._handleKeypressEvent);
});
};

_removeEventHandlers = () => {
this._activators.forEach((el: HTMLElement) => {
el.removeEventListener('click', this._handleClickInteraction);
el.removeEventListener('keypress', this._handleKeypressEvent);
});
};

_handleClickInteraction = () => {
this._handleRevealEvent();
};

_handleKeypressEvent: (e: KeyboardEvent) => void = (e: KeyboardEvent) => {
if (Utils.keys.ENTER.includes(e.key)) {
this._handleRevealEvent();
}
};

_handleRevealEvent = () => {
// Reveal Card
this._activators.forEach((el: HTMLElement) => el.tabIndex = -1);
this.open();
};

_setupRevealCloseEventHandlers = () => {
this.cardRevealClose.addEventListener('click', this.close);
this.cardRevealClose.addEventListener('keypress', this._handleKeypressCloseEvent);
};

_removeRevealCloseEventHandlers = () => {
this.cardRevealClose.addEventListener('click', this.close);
this.cardRevealClose.addEventListener('keypress', this._handleKeypressCloseEvent);
};

_handleKeypressCloseEvent: (e: KeyboardEvent) => void = (e: KeyboardEvent) => {
if (Utils.keys.ENTER.includes(e.key)) {
this.close();
}
};

/**
* Show card reveal.
*/
open: () => void = () => {
if (this.isOpen) return;
this.isOpen = true;
this.el.style.overflow = 'hidden';
this.cardReveal.style.display = 'block';
this.cardReveal.ariaExpanded = 'true';
this.cardRevealClose.tabIndex = 0;
setTimeout(() => {
this.cardReveal.style.transition = `transform ${this.options.outDuration}ms ease`; //easeInOutQuad
this.cardReveal.style.transform = 'translateY(-100%)';
}, 1);
if (typeof this.options.onOpen === 'function') {
this.options.onOpen.call(this);
}
this._setupRevealCloseEventHandlers();
};

/**
* Hide card reveal.
*/
close: () => void = () => {
if (!this.isOpen) return;
this.isOpen = false;
this.cardReveal.style.transition = `transform ${this.options.inDuration}ms ease`; //easeInOutQuad
this.cardReveal.style.transform = 'translateY(0)';
setTimeout(() => {
this.cardReveal.style.display = 'none';
this.cardReveal.ariaExpanded = 'false';
this._activators.forEach((el: HTMLElement) => el.tabIndex = 0);
this.cardRevealClose.tabIndex = -1;
this.el.style.overflow = this.initialOverflow;
}, this.options.inDuration);
if (typeof this.options.onClose === 'function') {
this.options.onClose.call(this);
}
this._removeRevealCloseEventHandlers();
};
}
6 changes: 4 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Autocomplete, AutocompleteOptions } from './autocomplete';
import { FloatingActionButton, FloatingActionButtonOptions } from './buttons';
import { Cards } from './cards';
import { Cards, CardsOptions } from './cards';
import { Carousel, CarouselOptions } from './carousel';
import { CharacterCounter, CharacterCounterOptions } from './characterCounter';
import { Chips, ChipsOptions } from './chips';
Expand Down Expand Up @@ -64,6 +64,7 @@ export function Button(children: any = '') {

export interface AutoInitOptions {
Autocomplete?: Partial<AutocompleteOptions>
Cards?: Partial<CardsOptions>
Carousel?: Partial<CarouselOptions>
Chips?: Partial<ChipsOptions>
Collapsible?: Partial<CollapsibleOptions>
Expand Down Expand Up @@ -91,6 +92,7 @@ export interface AutoInitOptions {
export function AutoInit(context: HTMLElement = document.body, options?: Partial<AutoInitOptions>) {
let registry = {
Autocomplete: context.querySelectorAll('.autocomplete:not(.no-autoinit)'),
Cards: context.querySelectorAll('.cards:not(.no-autoinit)'),
Carousel: context.querySelectorAll('.carousel:not(.no-autoinit)'),
Chips: context.querySelectorAll('.chips:not(.no-autoinit)'),
Collapsible: context.querySelectorAll('.collapsible:not(.no-autoinit)'),
Expand All @@ -110,6 +112,7 @@ export function AutoInit(context: HTMLElement = document.body, options?: Partial
FloatingActionButton: context.querySelectorAll('.fixed-action-btn:not(.no-autoinit)')
};
Autocomplete.init(registry.Autocomplete, options?.Autocomplete ?? {});
Cards.init(registry.Cards, options?.Cards ?? {})
Carousel.init(registry.Carousel, options?.Carousel ?? {});
Chips.init(registry.Chips, options?.Chips ?? {});
Collapsible.init(registry.Collapsible, options?.Collapsible ?? {});
Expand Down Expand Up @@ -137,7 +140,6 @@ if (typeof document !== 'undefined') {
document.addEventListener('focus', Utils.docHandleFocus, true);
document.addEventListener('blur', Utils.docHandleBlur, true);
}
Cards.Init();
Forms.Init();
Chips.Init();
Waves.Init();
Expand Down

0 comments on commit c24f233

Please sign in to comment.