Skip to content

Commit

Permalink
feat(CDK模块): 添加通用cdk常用模块
Browse files Browse the repository at this point in the history
  • Loading branch information
jiayisheji committed Feb 2, 2018
1 parent 5b14494 commit 1cafcda
Show file tree
Hide file tree
Showing 66 changed files with 5,270 additions and 0 deletions.
Empty file added src/app/cdk/README.md
Empty file.
26 changes: 26 additions & 0 deletions src/app/cdk/a11y/a11y.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';


import { PlatformModule } from '../platform';

import { ARIA_DESCRIBER_PROVIDER, AriaDescriber } from './aria-describer';
import { CdkMonitorFocusDirective, FOCUS_MONITOR_PROVIDER } from './focus-monitor';
import { CdkTrapFocusDirective, FocusTrapDeprecatedDirective, FocusTrapFactory } from './focus-trap';
import { InteractivityChecker } from './interactivity-checker';
import { LIVE_ANNOUNCER_PROVIDER } from './live-announcer';

@NgModule({
imports: [CommonModule, PlatformModule],
declarations: [CdkTrapFocusDirective, FocusTrapDeprecatedDirective, CdkMonitorFocusDirective],
exports: [CdkTrapFocusDirective, FocusTrapDeprecatedDirective, CdkMonitorFocusDirective],
providers: [
InteractivityChecker,
FocusTrapFactory,
AriaDescriber,
LIVE_ANNOUNCER_PROVIDER,
ARIA_DESCRIBER_PROVIDER,
FOCUS_MONITOR_PROVIDER,
]
})
export class A11yModule { }
33 changes: 33 additions & 0 deletions src/app/cdk/a11y/activedescendant-key-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ListKeyManager, ListKeyManagerOption } from './list-key-manager';

/**
* This is the interface for highlightable items (used by the ActiveDescendantKeyManager).
* Each item must know how to style itself as active or inactive and whether or not it is
* currently disabled.
*/
export interface Highlightable extends ListKeyManagerOption {
/** Applies the styles for an active item to this item. */
setActiveStyles(): void;

/** Applies the styles for an inactive item to this item. */
setInactiveStyles(): void;
}

export class ActiveDescendantKeyManager<T> extends ListKeyManager<Highlightable & T> {

/**
* This method sets the active item to the item at the specified index.
* It also adds active styles to the newly active item and removes active
* styles from the previously active item.
*/
setActiveItem(index: number): void {
if (this.activeItem) {
this.activeItem.setInactiveStyles();
}
super.setActiveItem(index);
if (this.activeItem) {
this.activeItem.setActiveStyles();
}
}

}
208 changes: 208 additions & 0 deletions src/app/cdk/a11y/aria-describer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { Injectable, Inject, InjectionToken, Optional, SkipSelf, OnDestroy } from '@angular/core';
import { DOCUMENT } from '@angular/common';

import { addAriaReferencedId, getAriaReferenceIds, removeAriaReferencedId } from './aria-reference';

/**
* Interface used to register message elements and keep a count of how many registrations have
* the same message and the reference to the message element used for the `aria-describedby`.
*/
export interface RegisteredMessage {
/** The element containing the message. */
messageElement: Element;

/** The number of elements that reference this message element via `aria-describedby`. */
referenceCount: number;
}

/** ID used for the body container where all messages are appended. */
export const MESSAGES_CONTAINER_ID = 'cdk-describedby-message-container';

/** ID prefix used for each created message element. */
export const CDK_DESCRIBEDBY_ID_PREFIX = 'cdk-describedby-message';

/** Attribute given to each host element that is described by a message element. */
export const CDK_DESCRIBEDBY_HOST_ATTRIBUTE = 'cdk-describedby-host';

/** Global incremental identifier for each registered message element. */
let nextId = 0;

/** Global map of all registered message elements that have been placed into the document. */
const messageRegistry = new Map<string, RegisteredMessage>();

/** Container for all registered messages. */
let messagesContainer: HTMLElement | null = null;

/**
* Utility that creates visually hidden elements with a message content. Useful for elements that
* want to use aria-describedby to further describe themselves without adding additional visual
* content.
* @docs-private
*/
@Injectable()
export class AriaDescriber implements OnDestroy {
private _document: Document;

constructor( @Inject(DOCUMENT) _document: any) {
this._document = _document;
}

/**
* Adds to the host element an aria-describedby reference to a hidden element that contains
* the message. If the same message has already been registered, then it will reuse the created
* message element.
*/
describe(hostElement: Element, message: string) {
if (!message.trim()) {
return;
}

if (!messageRegistry.has(message)) {
this._createMessageElement(message);
}

if (!this._isElementDescribedByMessage(hostElement, message)) {
this._addMessageReference(hostElement, message);
}
}

/** Removes the host element's aria-describedby reference to the message element. */
removeDescription(hostElement: Element, message: string) {
if (!message.trim()) {
return;
}

if (this._isElementDescribedByMessage(hostElement, message)) {
this._removeMessageReference(hostElement, message);
}

const registeredMessage = messageRegistry.get(message);
if (registeredMessage && registeredMessage.referenceCount === 0) {
this._deleteMessageElement(message);
}

if (messagesContainer && messagesContainer.childNodes.length === 0) {
this._deleteMessagesContainer();
}
}

/** Unregisters all created message elements and removes the message container. */
ngOnDestroy() {
const describedElements =
this._document.querySelectorAll(`[${CDK_DESCRIBEDBY_HOST_ATTRIBUTE}]`);

for (let i = 0; i < describedElements.length; i++) {
this._removeCdkDescribedByReferenceIds(describedElements[i]);
describedElements[i].removeAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE);
}

if (messagesContainer) {
this._deleteMessagesContainer();
}

messageRegistry.clear();
}

/**
* Creates a new element in the visually hidden message container element with the message
* as its content and adds it to the message registry.
*/
private _createMessageElement(message: string) {
const messageElement = this._document.createElement('div');
messageElement.setAttribute('id', `${CDK_DESCRIBEDBY_ID_PREFIX}-${nextId++}`);
messageElement.appendChild(this._document.createTextNode(message));

if (!messagesContainer) { this._createMessagesContainer(); }
messagesContainer.appendChild(messageElement);

messageRegistry.set(message, { messageElement, referenceCount: 0 });
}

/** Deletes the message element from the global messages container. */
private _deleteMessageElement(message: string) {
const registeredMessage = messageRegistry.get(message);
const messageElement = registeredMessage && registeredMessage.messageElement;
if (messagesContainer && messageElement) {
messagesContainer.removeChild(messageElement);
}
messageRegistry.delete(message);
}

/** Creates the global container for all aria-describedby messages. */
private _createMessagesContainer() {
messagesContainer = this._document.createElement('div');
messagesContainer.setAttribute('id', MESSAGES_CONTAINER_ID);
messagesContainer.setAttribute('aria-hidden', 'true');
messagesContainer.style.display = 'none';
this._document.body.appendChild(messagesContainer);
}

/** Deletes the global messages container. */
private _deleteMessagesContainer() {
if (messagesContainer && messagesContainer.parentNode) {
messagesContainer.parentNode.removeChild(messagesContainer);
messagesContainer = null;
}
}

/** Removes all cdk-describedby messages that are hosted through the element. */
private _removeCdkDescribedByReferenceIds(element: Element) {
// Remove all aria-describedby reference IDs that are prefixed by CDK_DESCRIBEDBY_ID_PREFIX
const originalReferenceIds = getAriaReferenceIds(element, 'aria-describedby')
.filter(id => id.indexOf(CDK_DESCRIBEDBY_ID_PREFIX) !== 0);
element.setAttribute('aria-describedby', originalReferenceIds.join(' '));
}

/**
* Adds a message reference to the element using aria-describedby and increments the registered
* message's reference count.
*/
private _addMessageReference(element: Element, message: string) {
const registeredMessage = messageRegistry.get(message);

// Add the aria-describedby reference and set the
// describedby_host attribute to mark the element.
addAriaReferencedId(element, 'aria-describedby', registeredMessage.messageElement.id);
element.setAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE, '');

registeredMessage.referenceCount++;
}

/**
* Removes a message reference from the element using aria-describedby
* and decrements the registered message's reference count.
*/
private _removeMessageReference(element: Element, message: string) {
const registeredMessage = messageRegistry.get(message);
registeredMessage.referenceCount--;

removeAriaReferencedId(element, 'aria-describedby', registeredMessage.messageElement.id);
element.removeAttribute(CDK_DESCRIBEDBY_HOST_ATTRIBUTE);
}

/** Returns true if the element has been described by the provided message ID. */
private _isElementDescribedByMessage(element: Element, message: string): boolean {
const referenceIds = getAriaReferenceIds(element, 'aria-describedby');
const registeredMessage = messageRegistry.get(message);
const messageId = registeredMessage && registeredMessage.messageElement.id;

return !!messageId && referenceIds.indexOf(messageId) !== -1;
}

}

/** @docs-private */
export function ARIA_DESCRIBER_PROVIDER_FACTORY(parentDispatcher: AriaDescriber, _document: any) {
return parentDispatcher || new AriaDescriber(_document);
}

/** @docs-private */
export const ARIA_DESCRIBER_PROVIDER = {
// If there is already an AriaDescriber available, use that. Otherwise, provide a new one.
provide: AriaDescriber,
deps: [
[new Optional(), new SkipSelf(), AriaDescriber],
DOCUMENT as InjectionToken<any>
],
useFactory: ARIA_DESCRIBER_PROVIDER_FACTORY
};
34 changes: 34 additions & 0 deletions src/app/cdk/a11y/aria-reference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/** IDs are deliminated by an empty space, as per the spec. */
const ID_DELIMINATOR = ' ';

/**
* Adds the given ID to the specified ARIA attribute on an element.
* Used for attributes such as aria-labelledby, aria-owns, etc.
*/
export function addAriaReferencedId(el: Element, attr: string, id: string) {
const ids = getAriaReferenceIds(el, attr);
if (ids.some(existingId => existingId.trim() === id.trim())) { return; }
ids.push(id.trim());

el.setAttribute(attr, ids.join(ID_DELIMINATOR));
}

/**
* Removes the given ID from the specified ARIA attribute on an element.
* Used for attributes such as aria-labelledby, aria-owns, etc.
*/
export function removeAriaReferencedId(el: Element, attr: string, id: string) {
const ids = getAriaReferenceIds(el, attr);
const filteredIds = ids.filter(val => val !== id.trim());

el.setAttribute(attr, filteredIds.join(ID_DELIMINATOR));
}

/**
* Gets the list of IDs referenced by the given ARIA attribute on an element.
* Used for attributes such as aria-labelledby, aria-owns, etc.
*/
export function getAriaReferenceIds(el: Element, attr: string): string[] {
// Get string array of all individual ids (whitespace deliminated) in the attribute value
return (el.getAttribute(attr) || '').match(/\S+/g) || [];
}
10 changes: 10 additions & 0 deletions src/app/cdk/a11y/fake-mousedown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Screenreaders will often fire fake mousedown events when a focusable element
* is activated using the keyboard. We can typically distinguish between these faked
* mousedown events and real mousedown events using the "buttons" property. While
* real mousedowns will indicate the mouse button that was pressed (e.g. "1" for
* the left mouse button), faked mousedowns will usually set the property value to 0.
*/
export function isFakeMousedownFromScreenReader(event: MouseEvent): boolean {
return event.buttons === 0;
}
25 changes: 25 additions & 0 deletions src/app/cdk/a11y/focus-key-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ListKeyManager, ListKeyManagerOption } from './list-key-manager';

/**
* This is the interface for focusable items (used by the FocusKeyManager).
* Each item must know how to focus itself, whether or not it is currently disabled
* and be able to supply it's label.
*/
export interface FocusableOption extends ListKeyManagerOption {
/** Focuses the `FocusableOption`. */
focus(): void;
}

export class FocusKeyManager<T> extends ListKeyManager<FocusableOption & T> {
/**
* This method sets the active item to the item at the specified index.
* It also adds focuses the newly active item.
*/
setActiveItem(index: number): void {
super.setActiveItem(index);

if (this.activeItem) {
this.activeItem.focus();
}
}
}

0 comments on commit 1cafcda

Please sign in to comment.