Skip to content

Commit

Permalink
feat(core): Add event delegation library to queue up events and repla…
Browse files Browse the repository at this point in the history
…y them when the application is ready (#55121)

This adds the JSAction library from the Wiz framework to core/primitives.

PR Close #55121
  • Loading branch information
iteriani authored and thePunderWoman committed Apr 2, 2024
1 parent 840c375 commit 666d646
Show file tree
Hide file tree
Showing 31 changed files with 8,069 additions and 0 deletions.
28 changes: 28 additions & 0 deletions packages/core/primitives/event-dispatch/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
load("//tools:defaults.bzl", "ts_library", "tsec_test")

package(default_visibility = ["//visibility:public"])

ts_library(
name = "event-dispatch",
srcs = glob(
[
"**/*.ts",
],
),
module_name = "@angular/core/primitives/event-dispatch",
)

tsec_test(
name = "tsec_test",
target = "event-dispatch",
tsconfig = "//packages:tsec_config",
)

filegroup(
name = "files_for_docgen",
srcs = glob([
"*.ts",
"src/**/*.ts",
]),
visibility = ["//visibility:public"],
)
13 changes: 13 additions & 0 deletions packages/core/primitives/event-dispatch/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/


export {Dispatcher, registerDispatcher} from './src/dispatcher';
export {EventContractContainer} from './src/event_contract_container';
export {EventContract} from './src/eventcontract';
export {bootstrapEventContract} from './src/register_events';
61 changes: 61 additions & 0 deletions packages/core/primitives/event-dispatch/src/a11y_click.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import * as eventLib from './event';
import * as eventInfoLib from './event_info';
import {EventType} from './event_type';

/**
* Update `EventInfo` to be `eventType = 'click'` and sets `a11yClickKey` if it
* is a a11y click.
*/
export function updateEventInfoForA11yClick(eventInfo: eventInfoLib.EventInfo) {
if (!eventLib.isActionKeyEvent(eventInfoLib.getEvent(eventInfo))) {
return;
}
eventInfoLib.setA11yClickKey(eventInfo, true);
// A 'click' triggered by a DOM keypress should be mapped to the 'click'
// jsaction.
eventInfoLib.setEventType(eventInfo, EventType.CLICK);
}

/**
* Call `preventDefault` on an a11y click if it is space key or to prevent the
* browser's default action for native HTML controls.
*/
export function preventDefaultForA11yClick(eventInfo: eventInfoLib.EventInfo) {
if (!eventInfoLib.getA11yClickKey(eventInfo) ||
(!eventLib.isSpaceKeyEvent(eventInfoLib.getEvent(eventInfo)) &&
!eventLib.shouldCallPreventDefaultOnNativeHtmlControl(
eventInfoLib.getEvent(eventInfo),
))) {
return;
}
eventLib.preventDefault(eventInfoLib.getEvent(eventInfo));
}

/**
* Sets the `action` to `clickonly` for a click event that is not an a11y click
* and if there is not already a click action.
*/
export function populateClickOnlyAction(
eventInfo: eventInfoLib.EventInfo,
actionMap: {[key: string]: string},
) {
if (eventInfoLib.getEventType(eventInfo) === EventType.CLICK &&
// No a11y clicks should map to 'clickonly'.
!eventInfoLib.getA11yClickKey(eventInfo) && !actionMap[EventType.CLICK] &&
actionMap[EventType.CLICKONLY]) {
// A 'click' triggered by a DOM click should be mapped to the 'click'
// jsaction, if available, or else fallback to the 'clickonly' jsaction.
// If 'click' and 'clickonly' jsactions are used together, 'click' will
// prevail.
eventInfoLib.setEventType(eventInfo, EventType.CLICKONLY);
eventInfoLib.setAction(eventInfo, actionMap[EventType.CLICKONLY]);
}
}
44 changes: 44 additions & 0 deletions packages/core/primitives/event-dispatch/src/accessibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

/**
* Defines special EventInfo and Event properties used when
* A11Y_SUPPORT_IN_DISPATCHER is enabled.
*/
export enum Attribute {
/**
* An event-type set when the event contract detects a KEYDOWN event but
* doesn't know if the key press can be treated like a click. The dispatcher
* will use this event-type to parse the keypress and handle it accordingly.
*/
MAYBE_CLICK_EVENT_TYPE = 'maybe_click',

/**
* A property added to a dispatched event that had the MAYBE_CLICK_EVENTTYPE
* event-type but could not be used as a click. The dispatcher sets this
* property for non-global dispatches before it retriggers the event and it
* signifies that the event contract should not dispatch this event globally.
*/
SKIP_GLOBAL_DISPATCH = 'a11ysgd',

/**
* A property added to a dispatched event that had the MAYBE_CLICK_EVENTTYPE
* event-type but could not be used as a click. The dispatcher sets this
* property before it retriggers the event and it signifies that the event
* contract should not look at CLICK actions for KEYDOWN events.
*/
SKIP_A11Y_CHECK = 'a11ysc',
}

declare global {
interface Event {
[Attribute.MAYBE_CLICK_EVENT_TYPE]?: boolean;
[Attribute.SKIP_GLOBAL_DISPATCH]?: boolean;
[Attribute.SKIP_A11Y_CHECK]?: boolean;
}
}
77 changes: 77 additions & 0 deletions packages/core/primitives/event-dispatch/src/attribute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

export enum Attribute {
/**
* The jsaction attribute defines a mapping of a DOM event to a
* generic event (aka jsaction), to which the actual event handlers
* that implement the behavior of the application are bound. The
* value is a semicolon separated list of colon separated pairs of
* an optional DOM event name and a jsaction name. If the optional
* DOM event name is omitted, 'click' is assumed. The jsaction names
* are dot separated pairs of a namespace and a simple jsaction
* name. If the namespace is absent, it is taken from the closest
* ancestor element with a jsnamespace attribute, if there is
* any. If there is no ancestor with a jsnamespace attribute, the
* simple name is assumed to be the jsaction name.
*
* Used by EventContract.
*/
JSACTION = 'jsaction',

/**
* The jsnamespace attribute provides the namespace part of the
* jaction names occurring in the jsaction attribute where it's
* missing.
*
* Used by EventContract.
*/
JSNAMESPACE = 'jsnamespace',

/**
* The oi attribute is a log impression tag for impression logging
* and action tracking. For an element that carries a jsaction
* attribute, the element is identified for the purpose of
* impression logging and click tracking by the dot separated path
* of all oi attributes in the chain of ancestors of the element.
*
* Used by ActionFlow.
*/
OI = 'oi',

/**
* The ved attribute is an encoded ClickTrackingCGI proto to track
* visual elements.
*
* Used by ActionFlow.
*/
VED = 'ved',

/**
* The vet attribute is the visual element type used to identify tracked
* visual elements.
*/
VET = 'vet',

/**
* Support for iteration on reprocessing.
*
* Used by ActionFlow.
*/
JSINSTANCE = 'jsinstance',

/**
* All click jsactions that happen on the element that carries this
* attribute or its descendants are automatically logged.
* Impressions of jsactions on these elements are tracked too, if
* requested by the impression() method of ActionFlow.
*
* Used by ActionFlow.
*/
JSTRACK = 'jstrack'
}
104 changes: 104 additions & 0 deletions packages/core/primitives/event-dispatch/src/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Property} from './property';

/**
* Map from jsaction annotation to a parsed map from event name to action name.
*/
const parseCache: {[key: string]: {[key: string]: string}} = {};

/**
* Reads the jsaction parser cache from the given DOM Element.
*
* @param element .
* @return Map from event to qualified name of the jsaction bound to it.
*/
export function get(element: Element): {[key: string]: string} {
// @ts-ignore
return element[Property.JSACTION];
}

/**
* Writes the jsaction parser cache to the given DOM Element.
*
* @param element .
* @param actionMap Map from event to qualified name of the jsaction bound to
* it.
*/
export function set(element: Element, actionMap: {[key: string]: string}) {
// @ts-ignore
element[Property.JSACTION] = actionMap;
}

/**
* Looks up the parsed action map from the source jsaction attribute value.
*
* @param text Unparsed jsaction attribute value.
* @return Parsed jsaction attribute value, if already present in the cache.
*/
export function getParsed(text: string): {[key: string]: string}|undefined {
return parseCache[text];
}

/**
* Inserts the parse result for the given source jsaction value into the cache.
*
* @param text Unparsed jsaction attribute value.
* @param parsed Attribute value parsed into the action map.
*/
export function setParsed(text: string, parsed: {[key: string]: string}) {
parseCache[text] = parsed;
}

/**
* Clears the jsaction parser cache from the given DOM Element.
*
* @param element .
*/
export function clear(element: Element) {
if (Property.JSACTION in element) {
delete element[Property.JSACTION];
}
}

/**
* Reads the cached jsaction namespace from the given DOM
* Element. Undefined means there is no cached value; null is a cached
* jsnamespace attribute that's absent.
*
* @param element .
* @return .
*/
export function getNamespace(element: Element): string|null|undefined {
// @ts-ignore
return element[Property.JSNAMESPACE];
}

/**
* Writes the cached jsaction namespace to the given DOM Element. Null
* represents a jsnamespace attribute that's absent.
*
* @param element .
* @param jsnamespace .
*/
export function setNamespace(element: Element, jsnamespace: string|null) {
// @ts-ignore
element[Property.JSNAMESPACE] = jsnamespace;
}

/**
* Clears the cached jsaction namespace from the given DOM Element.
*
* @param element .
*/
export function clearNamespace(element: Element) {
if (Property.JSNAMESPACE in element) {
delete element[Property.JSNAMESPACE];
}
}
39 changes: 39 additions & 0 deletions packages/core/primitives/event-dispatch/src/char.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

export const Char = {
/**
* The separator between the namespace and the action name in the
* jsaction attribute value.
*/
NAMESPACE_ACTION_SEPARATOR: '.',

/**
* The separator between the event name and action in the jsaction
* attribute value.
*/
EVENT_ACTION_SEPARATOR: ':',

/**
* The separator between the logged oi attribute values in the &oi=
* URL parameter value.
*/
OI_SEPARATOR: '.',

/**
* The separator between the key and the value pairs in the &cad=
* URL parameter value.
*/
CAD_KEY_VALUE_SEPARATOR: ':',

/**
* The separator between the key-value pairs in the &cad= URL
* parameter value.
*/
CAD_SEPARATOR: ',',
};
Loading

0 comments on commit 666d646

Please sign in to comment.