basic-component-mixins
Advanced tools
Comparing version 0.7.0 to 0.7.1
<a name="AttributeMarshalling"></a> | ||
## AttributeMarshalling | ||
Mixin which marshalls attributes to properties (and eventually | ||
vice versa) | ||
Mixin which marshalls attributes to properties (and eventually vice versa). | ||
This only supports string properties for now. | ||
If your component exposes a setter for a property, it's generally a good | ||
idea to let devs using your component be able to set that property in HTML | ||
via an element attribute. You can code that yourself by writing an | ||
`attributeChangedCallback`, or you can use this mixin to get a degree of | ||
automatic support. | ||
This mixin implements an `attributeChangedCallback` that will attempt to | ||
convert a change in an element attribute into a call to the corresponding | ||
property setter. Attributes typically follow hyphenated names ("foo-bar"), | ||
whereas properties typically use camelCase names ("fooBar"). This mixin | ||
respects that convention, automatically mapping the hyphenated attribute | ||
name to the corresponding camelCase property name. | ||
Example: You define a component using this mixin: | ||
class MyElement extends AttributeMarshalling(HTMLElement) { | ||
get fooBar() { return this._fooBar; } | ||
set fooBar(value) { this._fooBar = value; } | ||
} | ||
document.registerElement('my-element', MyElement); | ||
If someone then instantiates your component in HTML: | ||
<my-element foo-bar="Hello"></my-element> | ||
Then, after the element has been upgraded, the `fooBar` setter will | ||
automatically be invoked with the initial value "Hello". | ||
For the time being, this mixin only supports string-valued properties. | ||
If you'd like to convert string attributes to other types (numbers, | ||
booleans), you need to implement `attributeChangedCallback` yourself. | ||
**Kind**: global class |
<a name="ClickSelection"></a> | ||
## ClickSelection | ||
Mixin which maps a click (actually, a mousedown) to selection | ||
Mixin which maps a click (actually, a mousedown) to a selection. | ||
If the user clicks an element, and the element is an item in the list, then | ||
the component's selectedIndex will be set to the index for that item. | ||
This simple mixin is useful in list box-like elements, where a click on a | ||
list item implicitly selects it. | ||
This mixin expects the component to provide a method `indexOfItem(item)`. | ||
You can provide that method yourself, or use the ContentAsItems mixin. | ||
This mixin also expects the component to define a `selectedIndex` | ||
property. You can provide that yourself, or use the ItemsSelection mixin. | ||
**Kind**: global class |
<a name="Collective"></a> | ||
## Collective | ||
A group of elements that have been joined together for the purpose of | ||
A group of elements that have been associated for the purpose of | ||
accomplishing some collective behavior, e.g., keyboard handling. | ||
This is not a mixin, but a class used by the TargetInCollective mixin. | ||
There are certain components that want to cooperatively handle the keyboard. | ||
For example, the basic-arrow-selection and basic-page-dots components are | ||
optional components that can augment the appearance and behavior of an inner | ||
basic-carousel, adding arrow buttons and dot buttons, respectively. When | ||
these components are nested together, they form an implicit unit called a | ||
*collective*: | ||
<basic-arrow-selection> | ||
<basic-page-dots> | ||
<basic-carousel> | ||
... images, etc. ... | ||
</basic-carousel> | ||
</basic-page-dots> | ||
</basic-arrow-selection> | ||
In this configuration, the three components will all have a `this.collective` | ||
reference that refers to a shared instance of the `Collective` class. | ||
The Keyboard mixin they use is sensitive to the presence of | ||
the collective. Among other things, it will ensure that only the outermost | ||
element above — the basic-arrow-selection — will be a tab stop that can | ||
receive the keyboard focus. This lets the user perceive the component | ||
arrangement above as a single unit. The Keyboard mixin will also give each | ||
element in the collective a chance to process any keyboard events. So, even | ||
though the basic-arrow-selection element will have the focus, the standard | ||
keyboard navigation provided by basic-carousel will continue to work. | ||
The SelectionAriaActive component also respects collectives when using the | ||
`aria-activedescendant` and `role` attributes. Those will be applied to the | ||
outermost element (basic-arrow-selection, above) so that ARIA can correctly | ||
understand the arrangement of the elements. | ||
You can put elements into collectives yourself, or you can use the | ||
TargetInCollective mixin. | ||
**Kind**: global class | ||
* [Collective](#Collective) | ||
* [new Collective([elements])](#new_Collective_new) | ||
* [.elements](#Collective+elements) : <code>Array.<HTMLElement></code> | ||
* [.outermostElement](#Collective+outermostElement) | ||
* [.assimilate(target)](#Collective+assimilate) | ||
* [.invokeMethod(method, [args])](#Collective+invokeMethod) | ||
<a name="new_Collective_new"></a> | ||
### new Collective([elements]) | ||
Create a collective. | ||
| Param | Type | Description | | ||
| --- | --- | --- | | ||
| [elements] | <code>Array.<HTMLELement></code> | Initial elements to add. | | ||
<a name="Collective+elements"></a> | ||
### collective.elements : <code>Array.<HTMLElement></code> | ||
The elements in the collective. | ||
**Kind**: instance property of <code>[Collective](#Collective)</code> | ||
<a name="Collective+outermostElement"></a> | ||
### collective.outermostElement | ||
The outermost element in the collective. | ||
By convention, this is the first element in the `elements` array. | ||
**Kind**: instance property of <code>[Collective](#Collective)</code> | ||
<a name="Collective+assimilate"></a> | ||
### collective.assimilate(target) | ||
Add the indicated target to the collective. | ||
By convention, if two elements wants to participate in a collective, and | ||
one element is an ancestor of the other in the DOM, the ancestor should | ||
assimilate the descendant instead of the other way around. | ||
After assimilation, any element in the collective that defines a | ||
`collectiveChanged` method will have that method invoked. This allows | ||
the collective's elements to respond to changes in the collective. | ||
**Kind**: instance method of <code>[Collective](#Collective)</code> | ||
| Param | Type | Description | | ||
| --- | --- | --- | | ||
| target | <code>HTMLElement</code> | <code>[Collective](#Collective)</code> | The element or collective to add. | | ||
<a name="Collective+invokeMethod"></a> | ||
### collective.invokeMethod(method, [args]) | ||
Invoke a method on all elements in the collective. | ||
**Kind**: instance method of <code>[Collective](#Collective)</code> | ||
| Param | Type | Description | | ||
| --- | --- | --- | | ||
| method | <code>string</code> | The name of the method to invoke on all elements. | | ||
| [args] | <code>Array.<object></code> | The arguments to the method | | ||
<a name="Composable"></a> | ||
## Composable | ||
Mixin to make a class more easily composable with other mixins | ||
Mixin to make a class more easily composable with other mixins. | ||
The main contribution is the introduction of a `compose` method that applies | ||
a set of mixin functions and returns the resulting new class. This sugar | ||
can make the application of many mixins at once easier to read. | ||
This mixin contributes a `compose` method that applies a set of mixin | ||
functions and returns the resulting new class. This sugar can make the | ||
application of many mixins at once easier to read. | ||
**Kind**: global class | ||
<a name="undefinedcompose"></a> | ||
## undefinedcompose() | ||
<a name="Composable.compose"></a> | ||
### Composable.compose(...mixins) | ||
Apply a set of mixin functions or mixin objects to the present class and | ||
return the new class. | ||
A call like | ||
Instead of writing: | ||
let MyClass = Mixin1(Mixin2(Mixin3(Mixin4(Mixin5(BaseClass))))); | ||
Can be converted to: | ||
You can write: | ||
@@ -34,2 +34,10 @@ let MyClass = Composable(BaseClass).compose( | ||
**Kind**: global function | ||
In addition to providing syntactic sugar, this mixin can be used to | ||
define a class in ES5, which lacks ES6's `class` keyword. | ||
**Kind**: static method of <code>[Composable](#Composable)</code> | ||
| Param | Type | Description | | ||
| --- | --- | --- | | ||
| ...mixins | <code>mixins</code> | A set of mixin functions or objects to apply. | | ||
<a name="composeTemplates"></a> | ||
## composeTemplates | ||
Given two templates, this "folds" one inside the other | ||
## composeTemplates(baseTemplate, subTemplate) | ||
Given two templates, this "folds" one inside the other. This is | ||
is useful for defining a component that wants to fill in slots in the | ||
template of its base class. | ||
For now, the folding process just entails putting the first inside the | ||
location of the first <content> node in the second template. | ||
location of the first <slot> node in the second template. | ||
Example: if the first (sub) template is | ||
Example: if the first (base) template is | ||
<template> | ||
Hello, <slot></slot>. | ||
</template> | ||
<template> | ||
<b> | ||
<slot></slot> | ||
</b> | ||
</template> | ||
and the second (base) template is | ||
and the second (subclass) template is | ||
<template> | ||
<b> | ||
<slot></slot> | ||
</b> | ||
</template> | ||
<template> | ||
Hello, <slot></slot>. | ||
</template> | ||
Then the returned folded template is | ||
Then the result of calling `composeTemplates(first, second)` is | ||
<template> | ||
<b> | ||
Hello, <slot></slot>. | ||
</b> | ||
</template> | ||
<template> | ||
<b> | ||
Hello, <slot></slot>. | ||
</b> | ||
</template> | ||
**Kind**: global class | ||
Note that this function is not a mixin, but a helper for creating web | ||
components. | ||
**Kind**: global function | ||
| Param | Type | Description | | ||
| --- | --- | --- | | ||
| baseTemplate | <code>HTMLTemplate</code> | <code>string</code> | The base class template. | | ||
| subTemplate | <code>HTMLTemplate</code> | <code>string</code> | The subclass template. | | ||
<a name="ContentAsItems"></a> | ||
## ContentAsItems | ||
Mixin which maps content semantics (children) to list item | ||
semantics | ||
Mixin which maps content semantics (elements) to list item semantics. | ||
Items differ from children in several ways: | ||
This mixin expects a component to provide a `content` property returning a | ||
raw set of elements. You can provide that yourself, or use the | ||
`DistributedChildrenAsContent` mixin. | ||
Items differ from element contents in several ways: | ||
* They are often referenced via index. | ||
* They may have a selection state. | ||
* It's common to do work to initialize the appearance or state of a new item. | ||
* It's common to do work to initialize the appearance or state of a new | ||
item. | ||
* Auxiliary invisible child elements are filtered out and not counted as | ||
items. Auxiliary elements include link, script, style, and template | ||
elements. | ||
elements. This filtering ensures that those auxiliary elements can be | ||
used in markup inside of a list without being treated as list items. | ||
**Kind**: global class | ||
<a name="undefineditems"></a> | ||
## undefineditems | ||
The current set of items in the list. | ||
**Kind**: global variable | ||
**Properties** | ||
* [ContentAsItems](#ContentAsItems) | ||
* [.items](#ContentAsItems+items) : <code>Array.<HTMLElement></code> | ||
* [.applySelection(item, selected)](#ContentAsItems+applySelection) | ||
* [.indexOfItem(item)](#ContentAsItems+indexOfItem) ⇒ <code>number</code> | ||
* [.itemAdded(item)](#ContentAsItems+itemAdded) | ||
* [.itemsChanged()](#ContentAsItems+itemsChanged) | ||
| Name | Type | | ||
| --- | --- | | ||
| items | <code>object</code> | | ||
<a name="ContentAsItems+items"></a> | ||
### contentAsItems.items : <code>Array.<HTMLElement></code> | ||
The current set of items in the list. See the top-level documentation for | ||
mixin for a description of how items differ from plain content. | ||
<a name="indexOfItem"></a> | ||
## indexOfItem(item) ⇒ <code>number</code> | ||
Returns the positional index for the indicated item. | ||
**Kind**: instance property of <code>[ContentAsItems](#ContentAsItems)</code> | ||
<a name="ContentAsItems+applySelection"></a> | ||
### contentAsItems.applySelection(item, selected) | ||
Apply the selection state to a single item. | ||
Invoke this method to signal that the selected state of the indicated item | ||
has changed. By default, this applies a `selected` CSS class if the item | ||
is selected, and removed it if not selected. | ||
**Kind**: instance method of <code>[ContentAsItems](#ContentAsItems)</code> | ||
| Param | Type | Description | | ||
| --- | --- | --- | | ||
| item | <code>HTMLElement</code> | The item whose selection state has changed. | | ||
| selected | <code>boolean</code> | True if the item is selected, false if not. | | ||
<a name="ContentAsItems+indexOfItem"></a> | ||
### contentAsItems.indexOfItem(item) ⇒ <code>number</code> | ||
Return the positional index for the indicated item. | ||
Because this acts like a getter, this does not invoke a base implementation. | ||
**Kind**: global function | ||
**Kind**: instance method of <code>[ContentAsItems](#ContentAsItems)</code> | ||
**Returns**: <code>number</code> - The index of the item, or -1 if not found. | ||
@@ -38,4 +61,24 @@ | ||
| --- | --- | --- | | ||
| item | <code>object</code> | The item whose index is requested. | | ||
| item | <code>HTMLElement</code> | The item whose index is requested. | | ||
<a name="ContentAsItems+itemAdded"></a> | ||
### contentAsItems.itemAdded(item) | ||
This method is invoked whenever a new item is added to the list. | ||
The default implementation of this method does nothing. You can override | ||
this to perform per-item initialization. | ||
**Kind**: instance method of <code>[ContentAsItems](#ContentAsItems)</code> | ||
| Param | Type | Description | | ||
| --- | --- | --- | | ||
| item | <code>HTMLElement</code> | The item that was added. | | ||
<a name="ContentAsItems+itemsChanged"></a> | ||
### contentAsItems.itemsChanged() | ||
This method is invoked when the underlying contents change. It is also | ||
invoked on component initialization – since the items have "changed" from | ||
being nothing. | ||
**Kind**: instance method of <code>[ContentAsItems](#ContentAsItems)</code> | ||
<a name="event_items-changed"></a> | ||
@@ -42,0 +85,0 @@ ## "items-changed" |
<a name="ContentFirstChildTarget"></a> | ||
## ContentFirstChildTarget | ||
Mixin that defines the target of a component -- the element the | ||
component is managing or somehow responsible for -- as its first child | ||
Mixin that defines the target of a component — the element the component is | ||
managing or somehow responsible for — as its first child. | ||
Some components serve to decorate or modify other elements. A common | ||
pattern is to have one component wrap another, and have the outer, parent | ||
component implicitly modify the child. This mixin facilitates this by | ||
implicitly taking an element's first child as its "target". | ||
Example: | ||
<outer-element> | ||
<inner-element></inner-element> | ||
</outer-element> | ||
If `outer-element` uses this mixin, then its `target` property will be | ||
set to point to the `inner-element`, because that is its first child. | ||
This mixin expects a `content` property that returns the element's content. | ||
You can implement that yourself, or use the DistributedChildrenAsContent | ||
mixin. | ||
This mixin can be combined with the TargetInCollective mixin to have a | ||
component participate in collective keyboard handling. * | ||
**Kind**: global class | ||
<a name="ContentFirstChildTarget+target"></a> | ||
### contentFirstChildTarget.target : <code>HTMLElement</code> | ||
Gets/sets the current target of the component. | ||
**Kind**: instance property of <code>[ContentFirstChildTarget](#ContentFirstChildTarget)</code> |
<a name="DirectionSelection"></a> | ||
## DirectionSelection | ||
Mixin which maps direction semantics (goLeft, goRight, etc.) to | ||
selection semantics (selectPrevious, selectNext, etc.) | ||
Mixin which maps direction semantics (goLeft, goRight, etc.) to selection | ||
semantics (selectPrevious, selectNext, etc.). | ||
This mixin can be used in conjunction with the KeyboardDirection mixin | ||
(which maps keyboard events to directions) and a mixin that handles | ||
selection like ItemsSelection. | ||
**Kind**: global class |
<a name="DistributedChildren"></a> | ||
## DistributedChildren | ||
Mixin which defines helpers for accessing a component's | ||
distributed children as a flattened array or string. | ||
Mixin which defines helpers for accessing a component's distributed | ||
children as a flattened array or string. | ||
The standard DOM API provides several ways of accessing child content: | ||
`children`, `childNodes`, and `textContent`. None of these functions are | ||
Shadow DOM aware. This mixin defines variations of those functions that | ||
*are* Shadow DOM aware. | ||
Example: you create a component `<count-children>` that displays a number | ||
equal to the number of children placed inside that component. If someone | ||
instantiates your component like: | ||
<count-children> | ||
<div></div> | ||
<div></div> | ||
<div></div> | ||
</count-children> | ||
Then the component should show "3", because there are three children. To | ||
calculate the number of children, the component can just calculate | ||
`this.children.length`. However, suppose someone instantiates your | ||
component inside one of their own components, and puts a `<slot>` element | ||
inside your component: | ||
<count-children> | ||
<slot></slot> | ||
</count-children> | ||
If your component only looks at `this.children`, it will always see exactly | ||
one child — the `<slot>` element. But the user looking at the page will | ||
*see* any nodes distributed to that slot. To match what the user sees, your | ||
component should expand any `<slot>` elements it contains. | ||
That is the problem this mixin solves. After applying this mixin, your | ||
component code has access to `this.distributedChildren`, whose `length` | ||
will return the total number of all children distributed to your component | ||
in the composed tree. | ||
Note: The latest Custom Elements API design calls for a new function, | ||
`getAssignedNodes` that takes an optional `deep` parameter, that will solve | ||
this problem at the API level. | ||
**Kind**: global class | ||
* [DistributedChildren](#DistributedChildren) | ||
* [.distributedChildren](#DistributedChildren+distributedChildren) : <code>Array.<HTMLElement></code> | ||
* [.distributedChildNodes](#DistributedChildren+distributedChildNodes) : <code>Array.<Node></code> | ||
* [.distributedTextContent](#DistributedChildren+distributedTextContent) : <code>string</code> | ||
<a name="DistributedChildren+distributedChildren"></a> | ||
### distributedChildren.distributedChildren : <code>Array.<HTMLElement></code> | ||
An in-order collection of children, expanding any slot elements. Like the | ||
standard children property, this skips text nodes. | ||
**Kind**: instance property of <code>[DistributedChildren](#DistributedChildren)</code> | ||
<a name="DistributedChildren+distributedChildNodes"></a> | ||
### distributedChildren.distributedChildNodes : <code>Array.<Node></code> | ||
An in-order collection of child nodes, expanding any slot elements. Like | ||
the standard childNodes property, this includes text nodes. | ||
**Kind**: instance property of <code>[DistributedChildren](#DistributedChildren)</code> | ||
<a name="DistributedChildren+distributedTextContent"></a> | ||
### distributedChildren.distributedTextContent : <code>string</code> | ||
The concatenated text content of all child nodes, expanding any slot | ||
elements. | ||
**Kind**: instance property of <code>[DistributedChildren](#DistributedChildren)</code> |
<a name="DistributedChildrenAsContent"></a> | ||
## DistributedChildrenAsContent | ||
Mixin which defines a component's content as its children, | ||
including any nodes distributed to the component's slots. | ||
Mixin which defines a component's content as its children, expanding any | ||
nodes distributed to the component's slots. | ||
This mixin is intended for use with the DistributedChildren mixin. See that | ||
mixin for a discussion of how that works. This DistributedChildrenAsContent | ||
mixin provides an easy way of defining the "content" of a component as the | ||
component's distributed children. That in turn lets mixins like | ||
ContentAsItems manipulate the children as list items. | ||
**Kind**: global class | ||
<a name="undefinedcontent"></a> | ||
## undefinedcontent : <code>Array</code> | ||
<a name="DistributedChildrenAsContent+content"></a> | ||
### distributedChildrenAsContent.content : <code>Array.<HTMLElement></code> | ||
The content of this component, defined to be the flattened array of | ||
children distributed to the component. | ||
**Kind**: global variable | ||
**Properties** | ||
| Name | | ||
| --- | | ||
| content | | ||
**Kind**: instance property of <code>[DistributedChildrenAsContent](#DistributedChildrenAsContent)</code> |
<a name="Generic"></a> | ||
## Generic | ||
Mixin that allows a component to support a "generic" style: a | ||
minimalist style that can easily be removed to reset its visual appearance to | ||
a baseline state | ||
Mixin which allows a component to support a "generic" style: a minimalist | ||
style that can easily be removed to reset its visual appearance to a | ||
baseline state. | ||
@@ -11,20 +11,20 @@ By default, a component should provide a minimal visual presentation that | ||
in other settings. Each CSS rule has to be overridden. Worse, new CSS rules | ||
added to the default style won't be overridden by default, making it hard to | ||
know whether a new version of a component will still look okay. | ||
added to the default style won't be overridden by default, making it hard | ||
to know whether a new version of a component will still look okay. | ||
As a compromise, the simple Polymer behavior here defines a "generic" | ||
attribute. This attribute is normally set by default, and styles can be | ||
written that apply only when the generic attribute is set. This allows the | ||
construction of CSS rules that will only apply to generic components like | ||
As a compromise, the mixin defines a `generic` attribute. This attribute is | ||
normally set by default, and styles can be written that apply only when the | ||
generic attribute is set. This allows the construction of CSS rules that | ||
will only apply to generic components like: | ||
:host([generic=""]) { | ||
... | ||
... Generic appearance defined here ... | ||
} | ||
This makes it easy to remove all default styling -- set the generic attribute | ||
to false, and all default styling will be removed. | ||
This makes it easy to remove all default styling — set the `generic` | ||
attribute to false, and all default styling will be removed. | ||
**Kind**: global class | ||
<a name="undefinedgeneric"></a> | ||
## undefinedgeneric : <code>Boolean</code> | ||
<a name="Generic+generic"></a> | ||
### generic.generic : <code>Boolean</code> | ||
True if the component would like to receive generic styling. | ||
@@ -36,9 +36,3 @@ | ||
**Kind**: global variable | ||
**Kind**: instance property of <code>[Generic](#Generic)</code> | ||
**Default**: <code>true</code> | ||
**Properties** | ||
| Name | | ||
| --- | | ||
| generic | | ||
<a name="ItemsSelection"></a> | ||
## ItemsSelection | ||
Mixin which manages selection semantics for items in a list | ||
Mixin which manages single-selection semantics for items in a list. | ||
This mixin expects a component to provide an `items` array of all elements | ||
in the list. A standard way to do that with is the ContentAsItems mixin, | ||
which takes a component's content (typically its distributed children) as | ||
the set of list items; see that mixin for details. | ||
This mixin tracks a single selected item in the list, and provides means to | ||
get and set that state by item position (`selectedIndex`) or item identity | ||
(`selectedItem`). The selection can be moved in the list via the methods | ||
`selectFirst`, `selectLast`, `selectNext`, and `selectPrevious`. | ||
This mixin does not produce any user-visible effects to represent | ||
selection. Other mixins, such as SelectionAriaActive, SelectionHighlight | ||
and SelectionInView, modify the selected item in common ways to let the | ||
user know a given item is selected or not selected. | ||
**Kind**: global class | ||
<a name="undefinedselectedIndex"></a> | ||
## undefinedselectedIndex : <code>Number</code> | ||
* [ItemsSelection](#ItemsSelection) | ||
* [.canSelectNext](#ItemsSelection+canSelectNext) : <code>boolean</code> | ||
* [.canSelectPrevious](#ItemsSelection+canSelectPrevious) : <code>boolean</code> | ||
* [.selectedIndex](#ItemsSelection+selectedIndex) : <code>number</code> | ||
* [.selectedItem](#ItemsSelection+selectedItem) : <code>object</code> | ||
* [.selectionRequired](#ItemsSelection+selectionRequired) : <code>boolean</code> | ||
* [.applySelection(item, selected)](#ItemsSelection+applySelection) | ||
* [.itemAdded(item)](#ItemsSelection+itemAdded) | ||
* [.selectFirst()](#ItemsSelection+selectFirst) | ||
* [.selectLast()](#ItemsSelection+selectLast) | ||
* [.selectNext()](#ItemsSelection+selectNext) | ||
* [.selectPrevious()](#ItemsSelection+selectPrevious) | ||
<a name="ItemsSelection+canSelectNext"></a> | ||
### itemsSelection.canSelectNext : <code>boolean</code> | ||
True if the selection can be moved to the next item, false if not (the | ||
selected item is the last item in the list). | ||
**Kind**: instance property of <code>[ItemsSelection](#ItemsSelection)</code> | ||
<a name="ItemsSelection+canSelectPrevious"></a> | ||
### itemsSelection.canSelectPrevious : <code>boolean</code> | ||
True if the selection can be moved to the previous item, false if not | ||
(the selected item is the first one in the list). | ||
**Kind**: instance property of <code>[ItemsSelection](#ItemsSelection)</code> | ||
<a name="ItemsSelection+selectedIndex"></a> | ||
### itemsSelection.selectedIndex : <code>number</code> | ||
The index of the item which is currently selected, or -1 if there is no | ||
selection. | ||
**Kind**: global variable | ||
**Properties** | ||
Setting the index to -1 deselects any current-selected item. | ||
| Name | | ||
| --- | | ||
| selectedIndex | | ||
<a name="undefinedselectedItem"></a> | ||
## undefinedselectedItem : <code>Object</code> | ||
**Kind**: instance property of <code>[ItemsSelection](#ItemsSelection)</code> | ||
<a name="ItemsSelection+selectedItem"></a> | ||
### itemsSelection.selectedItem : <code>object</code> | ||
The currently selected item, or null if there is no selection. | ||
**Kind**: global variable | ||
**Properties** | ||
Setting this property to null deselects any currently-selected item. | ||
| Name | | ||
| --- | | ||
| selectedItem | | ||
<a name="undefinedselectionRequired"></a> | ||
## undefinedselectionRequired : <code>Boolean</code> | ||
**Kind**: instance property of <code>[ItemsSelection](#ItemsSelection)</code> | ||
<a name="ItemsSelection+selectionRequired"></a> | ||
### itemsSelection.selectionRequired : <code>boolean</code> | ||
True if the list should always have a selection (if it has items). | ||
**Kind**: global variable | ||
**Properties** | ||
**Kind**: instance property of <code>[ItemsSelection](#ItemsSelection)</code> | ||
<a name="ItemsSelection+applySelection"></a> | ||
### itemsSelection.applySelection(item, selected) | ||
Apply the indicate selection state to the item. | ||
| Name | | ||
| --- | | ||
| selectionRequired | | ||
The default implementation of this method does nothing. User-visible | ||
effects will typically be handled by other mixins. | ||
<a name="selectFirst"></a> | ||
## selectFirst() | ||
**Kind**: instance method of <code>[ItemsSelection](#ItemsSelection)</code> | ||
| Param | Type | Description | | ||
| --- | --- | --- | | ||
| item | <code>HTMLElement</code> | the item being selected/deselected | | ||
| selected | <code>boolean</code> | true if the item is selected, false if not | | ||
<a name="ItemsSelection+itemAdded"></a> | ||
### itemsSelection.itemAdded(item) | ||
Handle a new item being added to the list. | ||
The default implementation of this method simply sets the item's | ||
selection state to false. | ||
**Kind**: instance method of <code>[ItemsSelection](#ItemsSelection)</code> | ||
| Param | Type | Description | | ||
| --- | --- | --- | | ||
| item | <code>HTMLElement</code> | the item being added | | ||
<a name="ItemsSelection+selectFirst"></a> | ||
### itemsSelection.selectFirst() | ||
Select the first item in the list. | ||
**Kind**: global function | ||
<a name="selectLast"></a> | ||
## selectLast() | ||
**Kind**: instance method of <code>[ItemsSelection](#ItemsSelection)</code> | ||
<a name="ItemsSelection+selectLast"></a> | ||
### itemsSelection.selectLast() | ||
Select the last item in the list. | ||
**Kind**: global function | ||
<a name="selectNext"></a> | ||
## selectNext() | ||
**Kind**: instance method of <code>[ItemsSelection](#ItemsSelection)</code> | ||
<a name="ItemsSelection+selectNext"></a> | ||
### itemsSelection.selectNext() | ||
Select the next item in the list. | ||
**Kind**: global function | ||
<a name="selectPrevious"></a> | ||
## selectPrevious() | ||
**Kind**: instance method of <code>[ItemsSelection](#ItemsSelection)</code> | ||
<a name="ItemsSelection+selectPrevious"></a> | ||
### itemsSelection.selectPrevious() | ||
Select the previous item in the list. | ||
**Kind**: global function | ||
**Kind**: instance method of <code>[ItemsSelection](#ItemsSelection)</code> | ||
<a name="event_selected-item-changed"></a> | ||
@@ -66,6 +120,6 @@ ## "selected-item-changed" | ||
| Param | Description | | ||
| --- | --- | | ||
| detail.selectedItem | The new selected item. | | ||
| detail.previousItem | The previously selected item. | | ||
| Param | Type | Description | | ||
| --- | --- | --- | | ||
| detail.selectedItem | <code>HTMLElement</code> | The new selected item. | | ||
| detail.previousItem | <code>HTMLElement</code> | The previously selected item. | | ||
@@ -78,5 +132,5 @@ <a name="event_selected-item-changed"></a> | ||
| Param | Description | | ||
| --- | --- | | ||
| detail.selectedIndex | The new selected index. | | ||
| Param | Type | Description | | ||
| --- | --- | --- | | ||
| detail.selectedIndex | <code>number</code> | The new selected index. | | ||
<a name="Keyboard"></a> | ||
## Keyboard | ||
Mixin which manages the keydown handling for a component | ||
Mixin which manages the keydown handling for a component. | ||
TODO: Document collective behavior. | ||
TODO: Provide baseline behavior outside of a collective. | ||
This mixin handles several keyboard-related features. | ||
First, it wires up a single keydown event handler that can be shared by | ||
multiple mixins on a component. The event handler will invoke a `keydown` | ||
method with the event object, and any mixin along the prototype chain that | ||
wants to handle that method can do so. | ||
If a mixin wants to indicate that keyboard event has been handled, and that | ||
other mixins should *not* handle it, the mixin's `keydown` handler should | ||
return a value of true. The convention that seems to work well is that a | ||
mixin should see if it wants to handle the event and, if not, then ask the | ||
superclass to see if it wants to handle the event. This has the effect of | ||
giving the mixin that was applied last the first chance at handling a | ||
keyboard event. | ||
Example: | ||
keydown(event) { | ||
let handled; | ||
switch (event.keyCode) { | ||
// Handle the keys you want, setting handled = true if appropriate. | ||
} | ||
// Prefer mixin result if it's defined, otherwise use base result. | ||
return handled || (super.keydown && super.keydown(event)); | ||
} | ||
A second feature provided by this mixin is that it implicitly makes the | ||
component a tab stop if it isn't already, by setting `tabIndex` to 0. This | ||
has the effect of adding the component to the tab order in document order. | ||
Finally, this mixin is designed to work with Collective class via a mixin | ||
like TargetInCollective. This allows a set of related component instances | ||
to cooperatively handle the keyboard. See the Collective class for details. | ||
NOTE: For the time being, this mixin should be used with | ||
TargetInCollective. The intention is to allow this mixin to be used without | ||
requiring collective keyboard support, so that this mixin can be used on | ||
its own. | ||
**Kind**: global class | ||
<a name="Keyboard+keydown"></a> | ||
### keyboard.keydown(event) ⇒ <code>boolean</code> | ||
Handle the indicated keyboard event. | ||
The default implementation of this method does nothing. This will | ||
typically be handled by other mixins. | ||
**Kind**: instance method of <code>[Keyboard](#Keyboard)</code> | ||
**Returns**: <code>boolean</code> - true if the event was handled | ||
| Param | Type | Description | | ||
| --- | --- | --- | | ||
| event | <code>KeyboardEvent</code> | the keyboard event | | ||
<a name="KeyboardDirection"></a> | ||
## KeyboardDirection | ||
Mixin which maps direction keys (Left, Right, etc.) to direction | ||
semantics (goLeft, goRight, etc.) | ||
Mixin which maps direction keys (Left, Right, etc.) to direction semantics | ||
(go left, go right, etc.). | ||
This mixin expects the component to invoke a `keydown` method when a key is | ||
pressed. You can use the Keyboard mixin for that purpose, or wire up your | ||
own keyboard handling and call `keydown` yourself. | ||
This mixin calls methods such as `goLeft` and `goRight`. You can define | ||
what that means by implementing those methods yourself. If you want to use | ||
direction keys to navigate a selection, use this mixin with the | ||
DirectionSelection mixin. | ||
**Kind**: global class | ||
* [KeyboardDirection](#KeyboardDirection) | ||
* [.goDown()](#KeyboardDirection+goDown) | ||
* [.goEnd()](#KeyboardDirection+goEnd) | ||
* [.goLeft()](#KeyboardDirection+goLeft) | ||
* [.goRight()](#KeyboardDirection+goRight) | ||
* [.goStart()](#KeyboardDirection+goStart) | ||
* [.goUp()](#KeyboardDirection+goUp) | ||
<a name="KeyboardDirection+goDown"></a> | ||
### keyboardDirection.goDown() | ||
Invoked when the user wants to go/navigate down. | ||
The default implementation of this method does nothing. | ||
**Kind**: instance method of <code>[KeyboardDirection](#KeyboardDirection)</code> | ||
<a name="KeyboardDirection+goEnd"></a> | ||
### keyboardDirection.goEnd() | ||
Invoked when the user wants to go/navigate to the end (e.g., of a list). | ||
The default implementation of this method does nothing. | ||
**Kind**: instance method of <code>[KeyboardDirection](#KeyboardDirection)</code> | ||
<a name="KeyboardDirection+goLeft"></a> | ||
### keyboardDirection.goLeft() | ||
Invoked when the user wants to go/navigate left. | ||
The default implementation of this method does nothing. | ||
**Kind**: instance method of <code>[KeyboardDirection](#KeyboardDirection)</code> | ||
<a name="KeyboardDirection+goRight"></a> | ||
### keyboardDirection.goRight() | ||
Invoked when the user wants to go/navigate right. | ||
The default implementation of this method does nothing. | ||
**Kind**: instance method of <code>[KeyboardDirection](#KeyboardDirection)</code> | ||
<a name="KeyboardDirection+goStart"></a> | ||
### keyboardDirection.goStart() | ||
Invoked when the user wants to go/navigate to the start (e.g., of a | ||
list). The default implementation of this method does nothing. | ||
**Kind**: instance method of <code>[KeyboardDirection](#KeyboardDirection)</code> | ||
<a name="KeyboardDirection+goUp"></a> | ||
### keyboardDirection.goUp() | ||
Invoked when the user wants to go/navigate up. | ||
The default implementation of this method does nothing. | ||
**Kind**: instance method of <code>[KeyboardDirection](#KeyboardDirection)</code> |
<a name="KeyboardPagedSelection"></a> | ||
## KeyboardPagedSelection | ||
Mixin which maps page keys (Page Up, Page Down) into operations | ||
that move the selection by one page | ||
Mixin which maps page keys (Page Up, Page Down) into operations that move | ||
the selection by one page. | ||
@@ -18,27 +18,31 @@ The keyboard interaction model generally follows that of Microsoft Windows' | ||
To ensure the selected item is in view following use of Page Up/Down, use the | ||
related SelectionInView mixin. | ||
To ensure the selected item is in view following use of Page Up/Down, use | ||
the related SelectionInView mixin. | ||
This mixin expects the component to invoke a `keydown` method when a key is | ||
pressed. You can use the Keyboard mixin for that purpose, or wire up your | ||
own keyboard handling and call `keydown` yourself. | ||
**Kind**: global class | ||
<a name="undefinedscrollTarget"></a> | ||
## undefinedscrollTarget | ||
* [KeyboardPagedSelection](#KeyboardPagedSelection) | ||
* [.scrollTarget](#KeyboardPagedSelection+scrollTarget) : <code>HTMLElement</code> | ||
* [.pageDown()](#KeyboardPagedSelection+pageDown) | ||
* [.pageUp()](#KeyboardPagedSelection+pageUp) | ||
<a name="KeyboardPagedSelection+scrollTarget"></a> | ||
### keyboardPagedSelection.scrollTarget : <code>HTMLElement</code> | ||
The element that should be scrolled with the Page Up/Down keys. | ||
Default is the current element. | ||
**Kind**: global variable | ||
**Properties** | ||
| Name | | ||
| --- | | ||
| scrollTarget | | ||
<a name="pageDown"></a> | ||
## pageDown() | ||
**Kind**: instance property of <code>[KeyboardPagedSelection](#KeyboardPagedSelection)</code> | ||
<a name="KeyboardPagedSelection+pageDown"></a> | ||
### keyboardPagedSelection.pageDown() | ||
Scroll down one page. | ||
**Kind**: global function | ||
<a name="pageUp"></a> | ||
## pageUp() | ||
**Kind**: instance method of <code>[KeyboardPagedSelection](#KeyboardPagedSelection)</code> | ||
<a name="KeyboardPagedSelection+pageUp"></a> | ||
### keyboardPagedSelection.pageUp() | ||
Scroll up one page. | ||
**Kind**: global function | ||
**Kind**: instance method of <code>[KeyboardPagedSelection](#KeyboardPagedSelection)</code> |
<a name="KeyboardPrefixSelection"></a> | ||
## KeyboardPrefixSelection | ||
Mixin that handles list box-style prefix typing, in which the user | ||
can type a string to select the first item that begins with that string | ||
Mixin that handles list box-style prefix typing, in which the user can type | ||
a string to select the first item that begins with that string. | ||
Example: suppose a component using this mixin has the following items: | ||
<sample-list-component> | ||
<div>Apple</div> | ||
<div>Apricot</div> | ||
<div>Banana</div> | ||
<div>Blackberry</div> | ||
<div>Blueberry</div> | ||
<div>Cantaloupe</div> | ||
<div>Cherry</div> | ||
<div>Lemon</div> | ||
<div>Lime</div> | ||
</sample-list-component> | ||
If this component receives the focus, and the user presses the "b" or "B" | ||
key, the "Banana" item will be selected, because it's the first item that | ||
matches the prefix "b". (Matching is case-insensitive.) If the user now | ||
presses the "l" or "L" key quickly, the prefix to match becomes "bl", so | ||
"Blackberry" will be selected. | ||
The prefix typing feature has a one second timeout — the prefix to match | ||
will be reset after a second has passed since the user last typed a key. | ||
If, in the above example, the user waits a second between typing "b" and | ||
"l", the prefix will become "l", so "Lemon" would be selected. | ||
This mixin expects the component to invoke a `keydown` method when a key is | ||
pressed. You can use the Keyboard mixin for that purpose, or wire up your | ||
own keyboard handling and call `keydown` yourself. | ||
This mixin also expects the component to provide an `items` property. The | ||
`textContent` of those items will be used for purposes of prefix matching. | ||
**Kind**: global class | ||
<a name="selectItemWithTextPrefix"></a> | ||
## selectItemWithTextPrefix(prefix) | ||
<a name="KeyboardPrefixSelection+selectItemWithTextPrefix"></a> | ||
### keyboardPrefixSelection.selectItemWithTextPrefix(prefix) | ||
Select the first item whose text content begins with the given prefix. | ||
**Kind**: global function | ||
**Kind**: instance method of <code>[KeyboardPrefixSelection](#KeyboardPrefixSelection)</code> | ||
| Param | Description | | ||
| --- | --- | | ||
| prefix | [String] The string to search for | | ||
| prefix | [String] The prefix string to search for | | ||
@@ -1,7 +0,15 @@ | ||
<a name="microtask | ||
<a name="microtask"></a> | ||
## microtask(callback) | ||
Add a callback to the microtask queue. | ||
Adds a function to the microtask queue."></a> | ||
## microtask | ||
This uses a MutationObserver so that it works on IE 11. | ||
Adds a function to the microtask queue.() | ||
NOTE: IE 11 may actually use timeout timing with MutationObservers. This | ||
needs more investigation. | ||
**Kind**: global function | ||
| Param | Type | | ||
| --- | --- | | ||
| callback | <code>function</code> | | ||
<a name="ObserveContentChanges"></a> | ||
## ObserveContentChanges | ||
Wires up mutation observers to report any changes in a component's | ||
content (direct children, or nodes distributed to slots). | ||
Mixin which wires up mutation observers to report any changes in a | ||
component's content (direct children, or nodes distributed to slots). | ||
@@ -15,5 +15,27 @@ For the time being, this can only support a single level of distributed | ||
For comparison, see Polymer's observeNodes API, which does solve the problem | ||
of tracking changes in reprojected content. | ||
For comparison, see Polymer's observeNodes API, which does solve the | ||
problem of tracking changes in reprojected content. | ||
Note: The web platform team creating the specifications for web components | ||
plan to request that a new type of MutationObserver option be defined that | ||
lets a component monitor changes in distributed children. This mixin will | ||
be updated to take advantage of that MutationObserver option when that | ||
becomes available. | ||
**Kind**: global class | ||
<a name="ObserveContentChanges+contentChanged"></a> | ||
### observeContentChanges.contentChanged() | ||
Invoked when the contents of the component (including distributed | ||
children) have changed. | ||
This method is also invoked when a component is first instantiated; the | ||
contents have essentially "changed" from being nothing. This allows the | ||
component to perform initial processing of its children. | ||
**Kind**: instance method of <code>[ObserveContentChanges](#ObserveContentChanges)</code> | ||
<a name="event_content-changed"></a> | ||
## "content-changed" | ||
This event is raised when the component's contents (including distributed | ||
children) have changed. | ||
**Kind**: event emitted |
<a name="SelectionAriaActive"></a> | ||
## SelectionAriaActive | ||
Mixin which treats the selected item in a list as the active item | ||
in ARIA accessibility terms | ||
Mixin which treats the selected item in a list as the active item in ARIA | ||
accessibility terms. | ||
Handling ARIA selection state properly is actually quite complex. Not only | ||
does the selected item need to be marked as selected; the other items should | ||
be marked as *not* selected. Additionally, the outermost element with the | ||
keyboard focus needs to have attributes set on it so that the selection is | ||
knowable at the list level. That in turn requires that all items in the list | ||
have ID attributes assigned to them. (To that end, this mixin will assign | ||
generated IDs to any item that doesn't already have an ID.) | ||
Handling ARIA selection state properly is actually quite complex: | ||
* The items in the list need to be indicated as possible items via an ARIA | ||
`role` attribute value such as "option". | ||
* The selected item need to be marked as selected by setting the item's | ||
`aria-selected` attribute to true *and* the other items need be marked as | ||
*not* selected by setting `aria-selected` to false. | ||
* The outermost element with the keyboard focus needs to have attributes | ||
set on it so that the selection is knowable at the list level via the | ||
`aria-activedescendant` attribute. | ||
* Use of `aria-activedescendant` in turn requires that all items in the | ||
list have ID attributes assigned to them. | ||
This mixin tries to address all of the above requirements. To that end, | ||
this mixin will assign generated IDs to any item that doesn't already have | ||
an ID. | ||
ARIA relies on elements to provide `role` attributes. This mixin will apply | ||
a default role of "listbox" on the outer list if it doesn't already have an | ||
explicit role. Similarly, this mixin will apply a default role of "option" | ||
to any list item that does not already have a role specified. | ||
This mixin expects a set of members that manage the state of the selection: | ||
`applySelection`, `itemAdded`, and `selectedIndex`. You can supply these | ||
yourself, or do so via the ItemsSelection mixin. | ||
NOTE: For the time being, this mixin should be used with the | ||
TargetInCollective mixin. The intention is to eventually allow this mixin | ||
to be used without requiring collective keyboard support, so that this | ||
mixin can be used on its own. | ||
**Kind**: global class |
<a name="SelectionHighlight"></a> | ||
## SelectionHighlight | ||
Mixin which applies standard highlight colors to a selected item | ||
Mixin which applies standard highlight colors to a selected item. | ||
This mixin highlights textual items (e.g., in a list) in a standard way by | ||
using the CSS `highlight` and `highlighttext` color values. These values | ||
respect operating system defaults and user preferences, and hence are good | ||
default values for highlight colors. | ||
This mixin expects an `applySelection` method to be called on an item when | ||
its selected state changes. You can use the ItemsSelection mixin for that | ||
purpose. | ||
**Kind**: global class |
<a name="SelectionInView"></a> | ||
## SelectionInView | ||
Mixin which scrolls a container to keep the selected item visible | ||
Mixin which scrolls a container to ensure that a newly-selected item is | ||
visible to the user. | ||
When the selected item in a list-like component changes, it's easier for | ||
the to confirm that the selection has changed to an appropriate item if the | ||
user can actually see that item. | ||
This mixin expects a `selectedItem` property to be set when the selection | ||
changes. You can supply that yourself, or use the ItemsSelection mixin. | ||
**Kind**: global class | ||
<a name="undefinedscrollTarget"></a> | ||
## undefinedscrollTarget | ||
The element that should be scrolled with the Page Up/Down keys. | ||
Default is the current element. | ||
**Kind**: global variable | ||
**Properties** | ||
* [SelectionInView](#SelectionInView) | ||
* [.scrollTarget](#SelectionInView+scrollTarget) : <code>HTMLElement</code> | ||
* [.scrollItemIntoView(item)](#SelectionInView+scrollItemIntoView) | ||
| Name | | ||
| --- | | ||
| scrollTarget | | ||
<a name="SelectionInView+scrollTarget"></a> | ||
### selectionInView.scrollTarget : <code>HTMLElement</code> | ||
The element that should be scrolled to bring an item into view. | ||
<a name="scrollItemIntoView"></a> | ||
## scrollItemIntoView() | ||
The default value of this property is the element itself. | ||
**Kind**: instance property of <code>[SelectionInView](#SelectionInView)</code> | ||
<a name="SelectionInView+scrollItemIntoView"></a> | ||
### selectionInView.scrollItemIntoView(item) | ||
Scroll the given element completely into view, minimizing the degree of | ||
scrolling performed. | ||
Blink has a scrollIntoViewIfNeeded() function that almost the same thing, | ||
but unfortunately it's non-standard, and in any event often ends up | ||
scrolling more than is absolutely necessary. | ||
Blink has a `scrollIntoViewIfNeeded()` function that does something | ||
similar, but unfortunately it's non-standard, and in any event often ends | ||
up scrolling more than is absolutely necessary. | ||
**Kind**: global function | ||
**Kind**: instance method of <code>[SelectionInView](#SelectionInView)</code> | ||
| Param | Type | Description | | ||
| --- | --- | --- | | ||
| item | <code>HTMLElement</code> | the item to scroll into view. | | ||
<a name="ShadowElementReferences"></a> | ||
## ShadowElementReferences | ||
Mixin to create references to elements in a component's Shadow | ||
DOM subtree | ||
Mixin to create references to elements in a component's Shadow DOM subtree. | ||
This adds a member on the component called `$` that can be used to reference | ||
shadow elements with IDs. E.g., if component's shadow contains an element | ||
`<button id="foo">`, then this mixin will create a member `this.$.foo` that | ||
points to that button. Such references simplify a component's access to its | ||
own elements. | ||
This adds a member on the component called `this.$` that can be used to | ||
reference shadow elements with IDs. E.g., if component's shadow contains an | ||
element `<button id="foo">`, then this mixin will create a member | ||
`this.$.foo` that points to that button. | ||
This trades off a one-time cost of querying all elements in the shadow tree | ||
against having to query for an element each time the component wants to | ||
inspect or manipulate it. | ||
Such references simplify a component's access to its own elements. In | ||
exchange, this mixin trades off a one-time cost of querying all elements in | ||
the shadow tree instead of paying an ongoing cost to query for an element | ||
each time the component wants to inspect or manipulate it. | ||
This mixin is inspired by Polymer's automatic node finding feature. | ||
See https://www.polymer-project.org/1.0/docs/devguide/local-dom.html#node-finding. | ||
This mixin expects the component to define a Shadow DOM subtree. You can | ||
create that tree yourself, or make use of the ShadowTemplate mixin. | ||
This mixin is inspired by Polymer's [automatic | ||
node finding](https://www.polymer-project.org/1.0/docs/devguide/local-dom.html#node-finding) | ||
feature. | ||
**Kind**: global class |
<a name="ShadowTemplate"></a> | ||
## ShadowTemplate | ||
Mixin for stamping a template into a Shadow DOM subtree upon | ||
component instantiation | ||
Mixin for stamping a template into a Shadow DOM subtree upon component | ||
instantiation. | ||
If a component defines a template property (as a string or referencing a HTML | ||
template), when the component class is instantiated, a shadow root will be | ||
created on the instance, and the contents of the template will be cloned into | ||
the shadow root. | ||
To use this mixin, define a `template` property as a string or HTML | ||
`<template>` element: | ||
For the time being, this extension retains support for Shadow DOM v0. | ||
That will eventually be deprecated as browsers implement Shadow DOM v1. | ||
class MyElement extends ShadowTemplate(HTMLElement) { | ||
get template() { | ||
return `Hello, <em>world</em>.`; | ||
} | ||
} | ||
When your component class is instantiated, a shadow root will be created on | ||
the instance, and the contents of the template will be cloned into the | ||
shadow root. If your component does not define a `template` property, this | ||
mixin has no effect. | ||
For the time being, this extension retains support for Shadow DOM v0. That | ||
will eventually be deprecated as browsers (and the Shadow DOM polyfill) | ||
implement Shadow DOM v1. | ||
**Kind**: global class |
<a name="SwipeDirection"></a> | ||
## SwipeDirection | ||
Mixin which maps touch gestures (swipe left, swipe right) to direction | ||
semantics (goRight, goLeft) | ||
semantics (go right, go left). | ||
By default, this mixin presents no user-visible effects; it just indicates a | ||
direction in which the user is currently swiping or has finished swiping. To | ||
map the direction to a change in selection, use the DirectionSelection mixin. | ||
**Kind**: global class | ||
<a name="undefinedposition"></a> | ||
## undefinedposition : <code>Number</code> | ||
* [SwipeDirection](#SwipeDirection) | ||
* [.position](#SwipeDirection+position) : <code>number</code> | ||
* [.goLeft()](#SwipeDirection+goLeft) | ||
* [.goRight()](#SwipeDirection+goRight) | ||
* [.showTransition(value)](#SwipeDirection+showTransition) | ||
<a name="SwipeDirection+position"></a> | ||
### swipeDirection.position : <code>number</code> | ||
The distance the user has moved the first touchpoint since the beginning | ||
of a drag, expressed as a fraction of the element's width. | ||
**Kind**: global variable | ||
**Properties** | ||
**Kind**: instance property of <code>[SwipeDirection](#SwipeDirection)</code> | ||
<a name="SwipeDirection+goLeft"></a> | ||
### swipeDirection.goLeft() | ||
Invoked when the user wants to go/navigate left. | ||
The default implementation of this method does nothing. | ||
| Name | | ||
| --- | | ||
| position | | ||
**Kind**: instance method of <code>[SwipeDirection](#SwipeDirection)</code> | ||
<a name="SwipeDirection+goRight"></a> | ||
### swipeDirection.goRight() | ||
Invoked when the user wants to go/navigate right. | ||
The default implementation of this method does nothing. | ||
**Kind**: instance method of <code>[SwipeDirection](#SwipeDirection)</code> | ||
<a name="SwipeDirection+showTransition"></a> | ||
### swipeDirection.showTransition(value) | ||
Determine whether a transition should be shown during a swipe. | ||
Components like carousels often define animated CSS transitions for | ||
sliding effects. Such a transition should usually *not* be applied while | ||
the user is dragging, because a CSS animation will introduce a lag that | ||
makes the swipe feel sluggish. Instead, as long as the user is dragging | ||
with their finger down, the transition should be suppressed. When the | ||
user releases their finger, the transition can be restored, allowing the | ||
animation to show the carousel sliding into its final position. | ||
**Kind**: instance method of <code>[SwipeDirection](#SwipeDirection)</code> | ||
| Param | Type | Description | | ||
| --- | --- | --- | | ||
| value | <code>boolean</code> | true if a component-provided transition should be shown, false if not. | | ||
<a name="TargetInCollective"></a> | ||
## TargetInCollective | ||
Mixin which allows a component to provide aggregate behavior with | ||
other elements, e.g., for keyboard handling | ||
Mixin which allows a component to provide aggregate behavior with other | ||
elements, e.g., for keyboard handling. | ||
This mixin implicitly creates a collective for a component so that it can | ||
participate in collective keyboard handling. See the Collective class for | ||
details. | ||
You can use this mixin in conjunction with ContentFirstChildTarget to | ||
automatically have the component's collective extended to its first child. | ||
**Kind**: global class | ||
<a name="TargetInCollective+target"></a> | ||
### targetInCollective.target : <code>HTMLElement</code> | ||
Gets/sets the current target of the component. | ||
Set this to point to another element. That target element will be | ||
implicitly added to the component's collective. That is, the component | ||
and its target will share responsibility for handling keyboard events. | ||
You can set this property yourself, or you can use the | ||
ContentFirstChildTarget mixin to automatically set the target to the | ||
component's first child. | ||
**Kind**: instance property of <code>[TargetInCollective](#TargetInCollective)</code> |
<a name="TargetSelection"></a> | ||
## TargetSelection | ||
Mixin that allows a component to delegate its own selection | ||
semantics to a target element | ||
Mixin which allows a component to delegate its own selection semantics to a | ||
target element. | ||
This is useful when defining components that act as optional decorators for a | ||
component that acts like a list. | ||
This is useful when defining components that act as optional features for a | ||
component that acts like a list. See basic-arrow-selection and | ||
basic-page-dots for examples of components used as optional features for | ||
components like basic-carousel. A typical usage might be: | ||
<basic-arrow-selection> | ||
<basic-carousel> | ||
... images, etc. ... | ||
</basic-carousel> | ||
</basic-arrow-selection> | ||
Because basic-arrow-selection uses the TargetSelection mixin, it exposes | ||
members to access a selection: `selectNext`, `selectPrevious`, | ||
`selectedIndex`, etc. These are all delegated to the child component (here, | ||
a basic-carousel). | ||
This mixin expects a `target` property to be set to the element actually | ||
managing the selection. You can set that property yourself, or you can use | ||
the ContentFirstChildTarget mixin to implicitly take the component's first | ||
child as the target. This is what basic-arrow-selection (above) does. | ||
**Kind**: global class | ||
<a name="undefinedselectedIndex"></a> | ||
## undefinedselectedIndex : <code>Number</code> | ||
* [TargetSelection](#TargetSelection) | ||
* [.items](#TargetSelection+items) : <code>Array.<HTMLElement></code> | ||
* [.selectedIndex](#TargetSelection+selectedIndex) : <code>number</code> | ||
* [.selectedItem](#TargetSelection+selectedItem) : <code>HTMLElement</code> | ||
* [.target](#TargetSelection+target) : <code>HTMLElement</code> | ||
* [.indexOfItem(item)](#TargetSelection+indexOfItem) ⇒ <code>number</code> | ||
* [.itemsChanged()](#TargetSelection+itemsChanged) | ||
<a name="TargetSelection+items"></a> | ||
### targetSelection.items : <code>Array.<HTMLElement></code> | ||
The current set of items in the list. | ||
**Kind**: instance property of <code>[TargetSelection](#TargetSelection)</code> | ||
<a name="TargetSelection+selectedIndex"></a> | ||
### targetSelection.selectedIndex : <code>number</code> | ||
The index of the item which is currently selected, or -1 if there is no | ||
selection. | ||
**Kind**: global variable | ||
**Properties** | ||
**Kind**: instance property of <code>[TargetSelection](#TargetSelection)</code> | ||
<a name="TargetSelection+selectedItem"></a> | ||
### targetSelection.selectedItem : <code>HTMLElement</code> | ||
The currently selected item, or null if there is no selection. | ||
| Name | | ||
| --- | | ||
| selectedIndex | | ||
**Kind**: instance property of <code>[TargetSelection](#TargetSelection)</code> | ||
<a name="TargetSelection+target"></a> | ||
### targetSelection.target : <code>HTMLElement</code> | ||
Gets/sets the target element to which this component will delegate | ||
selection actions. | ||
<a name="undefinedselectedItem"></a> | ||
## undefinedselectedItem : <code>Object</code> | ||
The currently selected item, or null if there is no selection. | ||
**Kind**: instance property of <code>[TargetSelection](#TargetSelection)</code> | ||
<a name="TargetSelection+indexOfItem"></a> | ||
### targetSelection.indexOfItem(item) ⇒ <code>number</code> | ||
Return the positional index for the indicated item. | ||
**Kind**: global variable | ||
**Properties** | ||
**Kind**: instance method of <code>[TargetSelection](#TargetSelection)</code> | ||
**Returns**: <code>number</code> - The index of the item, or -1 if not found. | ||
| Name | | ||
| --- | | ||
| selectedItem | | ||
| Param | Type | Description | | ||
| --- | --- | --- | | ||
| item | <code>HTMLElement</code> | The item whose index is requested. | | ||
<a name="TargetSelection+itemsChanged"></a> | ||
### targetSelection.itemsChanged() | ||
This method is invoked when the underlying contents change. It is also | ||
invoked on component initialization – since the items have "changed" from | ||
being nothing. | ||
**Kind**: instance method of <code>[TargetSelection](#TargetSelection)</code> |
<a name="TimerSelection"></a> | ||
## TimerSelection | ||
Mixin provides for automatic timed changes in selection, as in a | ||
automated slideshow | ||
Mixin which provides for automatic timed changes in selection. | ||
This mixin is useful for creating slideshow-like elements. | ||
This mixin expects the component to define an `items` property, as well as | ||
`selectFirst` and `selectNext` methods. You can implement those yourself, | ||
or use the ContentAsItems and ItemsSelection mixins. | ||
**Kind**: global class | ||
<a name="undefinedplaying"></a> | ||
## undefinedplaying : <code>Boolean</code> | ||
True if the selection is being automatically advanced. | ||
**Kind**: global variable | ||
**Properties** | ||
* [TimerSelection](#TimerSelection) | ||
* [.playing](#TimerSelection+playing) : <code>boolean</code> | ||
* [.play()](#TimerSelection+play) | ||
* [.pause()](#TimerSelection+pause) | ||
| Name | | ||
| --- | | ||
| playing | | ||
<a name="TimerSelection+playing"></a> | ||
### timerSelection.playing : <code>boolean</code> | ||
True if the selection is being automatically advanced. | ||
<a name="play"></a> | ||
## play() | ||
**Kind**: instance property of <code>[TimerSelection](#TimerSelection)</code> | ||
<a name="TimerSelection+play"></a> | ||
### timerSelection.play() | ||
Begin automatic progression of the selection. | ||
**Kind**: global function | ||
<a name="pause"></a> | ||
## pause() | ||
**Kind**: instance method of <code>[TimerSelection](#TimerSelection)</code> | ||
<a name="TimerSelection+pause"></a> | ||
### timerSelection.pause() | ||
Pause automatic progression of the selection. | ||
**Kind**: global function | ||
**Kind**: instance method of <code>[TimerSelection](#TimerSelection)</code> |
<a name="TrackpadDirection"></a> | ||
## TrackpadDirection | ||
Mixin which maps a horizontal trackpad swipe gestures (or | ||
horizontal mouse wheel actions) to direction semantics | ||
Mixin which maps a horizontal trackpad swipe gestures (or horizontal mouse | ||
wheel actions) to direction semantics. | ||
To respond to the trackpad, we can listen to the DOM's "wheel" events. These | ||
events are fired as the user drags their fingers across a trackpad. | ||
Unfortunately, this scheme is missing a critical event — there is no event | ||
when the user *stops* a gestured on the trackpad. | ||
You can use this mixin with a mixin like DirectionSelection to let the user | ||
change the selection with the trackpad or mouse wheel. | ||
To complicate matters, the mainstream browsers continue to generate wheel | ||
events even after the user has stopped dragging their fingers. These fake | ||
events simulate the user gradually slowing down the drag until they come to a | ||
smooth stop. In some contexts, these fake wheel events might be helpful, but | ||
in trying to supply typical trackpad swipe navigation, these fake events get | ||
in the way. | ||
To respond to the trackpad, we can listen to the DOM's "wheel" events. | ||
These events are fired as the user drags their fingers across a trackpad. | ||
Unfortunately, browsers are missing a critical event — there is no event | ||
when the user *stops* a gestured on the trackpad or mouse wheel. | ||
This component uses some heuristics to work around these problems, but the | ||
complex nature of the problem make it extremely difficult to achieve the same | ||
degree of trackpad responsiveness possible with native applications. | ||
To make things worse, the mainstream browsers continue to generate fake | ||
wheel events even after the user has stopped dragging their fingers. These | ||
fake events simulate the user gradually slowing down the drag until they | ||
come to a smooth stop. In some contexts, these fake wheel events might be | ||
helpful, but in trying to supply typical trackpad swipe navigation, these | ||
fake events get in the way. | ||
This component uses heuristics to work around these problems, but the | ||
complex nature of the problem make it extremely difficult to achieve the | ||
same degree of trackpad responsiveness possible with native applications. | ||
**Kind**: global class | ||
* [TrackpadDirection](#TrackpadDirection) | ||
* [.position](#TrackpadDirection+position) : <code>number</code> | ||
* [.goLeft()](#TrackpadDirection+goLeft) | ||
* [.goRight()](#TrackpadDirection+goRight) | ||
<a name="TrackpadDirection+position"></a> | ||
### trackpadDirection.position : <code>number</code> | ||
The distance the user has moved the first touchpoint since the beginning | ||
of a trackpad/wheel operation, expressed as a fraction of the element's | ||
width. | ||
**Kind**: instance property of <code>[TrackpadDirection](#TrackpadDirection)</code> | ||
<a name="TrackpadDirection+goLeft"></a> | ||
### trackpadDirection.goLeft() | ||
Invoked when the user wants to go/navigate left. | ||
The default implementation of this method does nothing. | ||
**Kind**: instance method of <code>[TrackpadDirection](#TrackpadDirection)</code> | ||
<a name="TrackpadDirection+goRight"></a> | ||
### trackpadDirection.goRight() | ||
Invoked when the user wants to go/navigate right. | ||
The default implementation of this method does nothing. | ||
**Kind**: instance method of <code>[TrackpadDirection](#TrackpadDirection)</code> |
{ | ||
"name": "basic-component-mixins", | ||
"version": "0.7.0", | ||
"version": "0.7.1", | ||
"description": "Mixins for creating web components in plain JavaScript", | ||
@@ -5,0 +5,0 @@ "homepage": "https://component.kitchen", |
263
README.md
@@ -1,38 +0,59 @@ | ||
This package implements common web component features as mixins. It uses mixins | ||
to achieve the same results as a monolithic component framework, while | ||
permitting more flexibility and a pay-as-you-go approach to complexity and | ||
performance. | ||
# basic-component-mixins | ||
Design goals: | ||
Mixin library for creating web components in plain JavaScript (ES5 or ES6) | ||
1. Have each web component mixins focus on solving a single, common task. They | ||
should be well-factored. They should be able to be used on their own, or in | ||
combination. | ||
2. Introduce as few new concepts as possible. Any developer who understands the | ||
DOM API should find this architecture appealing, without having to learn many | ||
proprietary concepts (beyond mixins, see below). | ||
3. Focus on native browser support for ES6 and web components. The architecture | ||
should be useful in a production application today, but should also feel | ||
correct in a future world in which native ES6 and web components are | ||
everywhere. | ||
[![npm version](https://img.shields.io/npm/v/basic-component-mixins.svg?style=flat)](https://www.npmjs.com/package/basic-component-mixins) | ||
This library implements common web component features as JavaScript mixins. It | ||
is designed for people who would like to create web components in plain | ||
JavaScript while avoiding much of the boilerplate that comes up in component | ||
creation. The mixins permit flexibility and a pay-as-you-go approach to | ||
complexity and performance. | ||
# Building | ||
Design goals: | ||
After cloning this repository: | ||
1. **Focus each mixin on solving a single, common component task.** | ||
Each mixin should be useful on its own, or in combination. | ||
2. **Introduce as few new concepts as possible.** | ||
A developer who understands the DOM API should be able to work with these | ||
mixins without having to substantially change the way they write code. They | ||
shouldn't have to learn many proprietary concepts beyond those listed below, | ||
chiefly the notion of defining a mixin as a function. | ||
3. **Anticipate native browser support for ES6 and web components.** | ||
The architecture should be useful in an ES5 application today, but should | ||
also feel correct in a future world in which native ES6 and web components | ||
are everywhere. | ||
> npm install | ||
> grunt build | ||
All of the top-level Basic Web Components are constructed with these mixins. In | ||
fact, by design, most of those components are little more than combinations of | ||
mixins. That factoring allows you to create your own web components in the | ||
likely event that your needs differ from those driving the design of the Basic | ||
Web Components. You can use these mixins without using those components. | ||
# Composing web component classes with mixins as functions | ||
# Mixin concepts | ||
Web components can be expressed as compositions of base classes and mixins. | ||
Mixins here are defined as a function that takes a base class and returns a | ||
subclass defining the new features: | ||
## Mixins as functions | ||
The mixins in this library all take the form of a function. Each function takes | ||
a base class and returns a subclass defining the desired features: | ||
let MyMixin = (base) => class MyMixin extends base { | ||
// Mixin defines properties and methods here. | ||
greet() { | ||
return "Hello"; | ||
} | ||
}; | ||
class MyBaseClass {} | ||
let NewClass = MyMixin(MyBaseClass); | ||
let obj = new NewClass(); | ||
obj.greet(); // "Hello" | ||
Many JavaScript mixin implementations destructively modify a class prototype, | ||
but mixins of the functional style shown above do not. Rather, functional mixins | ||
extend the prototype chain and return a new class. Such functions have been | ||
called "higher-order components", but we prefer the term "mixin" for brevity. | ||
The mixins in this package take care to ensure that base class properties and | ||
@@ -46,16 +67,188 @@ methods are not broken by the mixin. In particular, if a mixin wants to add a | ||
A virtue of a functional mixin is that you do not need to use any library to | ||
apply it. This increases the chance that mixins can be shared across projects. | ||
If a common extension/mixin solution can be agreed upon, frameworks sharing that | ||
solution gain a certain degree of code sharing, interoperability, and can share | ||
conceptual materials. This reduces the learning curve for dealing with any one | ||
framework. | ||
A core virtue of a functional mixin is that you do not need to use any library | ||
to apply it. This lets you use these mixins with any conventional means of | ||
defining JavaScript classes — you don't have to invoke a proprietary class | ||
factory, nor do you have to load a separate framework or runtime. | ||
Frameworks can still make their own decisions about which features they want to | ||
offer by virtue of which mixins they incorporate into their base classes. | ||
Because mixins define behavior through composition, you're not limited by the | ||
constraints of a single-inheritance class hierarchy. That said, you can still | ||
use a class hierarchy if you feel that's suitable for your application. For | ||
example, you can compose a set of mixins to create a custom base class from | ||
which your other classes derive. But the use of such a base class is not | ||
dictated here. | ||
## Semantic mixin factoring | ||
In a number of areas, this package factors high-level component services into | ||
mixins that work together to deliver the overall service. This is done to | ||
increase flexibility. | ||
For example, this library includes three mixins that work in concert. When | ||
applied to a custom element, these mixins take care of mapping presses on | ||
keyboard arrows (Left/Right) into selection actions (select previous/next). | ||
They each take care of a different piece of the problem: | ||
* The [Keyboard](docs/Keyboard.md) mixin wires up a single keydown listener on | ||
the component that can be shared by multiple mixins. When the component has | ||
the focus, a keypress will result in the invocation of a `keydown` method. By | ||
default, that method does nothing. | ||
* The [KeyboardDirection](docs/KeyboardDirection.md) mixin maps keyboard | ||
semantics to direction semantics. It defines a `keydown` method that maps | ||
Left/Right arrow key presses into calls to methods `goLeft` and `goRight`, | ||
respectively. By default, those methods do nothing. | ||
* The [DirectionSelection](docs/DirectionSelection.md) mixin maps direction | ||
semantics to selection semantics. It defines `goLeft` and `goRight` methods | ||
which respectively invoke methods `selectPrevious` and `selectNext`. Again, by | ||
default, those methods do nothing. | ||
If all three mixins are applied to a component, then when the user presses, say, | ||
the Right arrow key, the following sequence happens: | ||
(keyboard event) → keydown() → goRight() → selectNext() | ||
Other mixins can map selection semantics to user-visible effects, such as | ||
highlighting the selected item, ensure the selected item is in view, or do | ||
something entirely new which you define. | ||
Such factoring may initially feel overly complex, but permits a critical degree | ||
of developer freedom. You might want to handle the keyboard a different way, | ||
for example. Or you may want to create a component that handles arrow | ||
keypresses for something other than selection, for example. Or you may want to | ||
let the user manipulate the selection through other modalities, such as touch | ||
gestures, mouse actions, speech commands, etc. | ||
As one example of another mode of user input, the | ||
[SwipeDirection](docs/SwipeDirection.md) mixin maps touch gestures to `goLeft` | ||
and `goRight` method calls. It can therefore be used in combination with the | ||
DirectionSelection mixin above, with the result that swipes will change the | ||
selection: | ||
(touch event) → goRight() → selectNext() | ||
The SwipeDirection and KeyboardDirection mixins are compatible, and can be | ||
applied to the same component. Users of that component will be able to change | ||
the selection with both touch gestures and the keyboard. | ||
This factoring allows components with radically different presentations to | ||
nevertheless share a considerable amount of user interface logic. For example, | ||
the [basic-carousel](../packages/basic-carousel) and | ||
[basic-list-box](packages/basic-list-box) components look very different, but | ||
both make use of same mixins to support changing the selection with the | ||
keyboard. In fact, nearly all of those components' behavior is defined through | ||
shared mixins factored this way. | ||
# Using the mixins to create web components | ||
## Installation of the mixins package via npm | ||
Add a `dependencies` key for `basic-component-mixins` in your project's | ||
package.json file. Until native Shadow DOM support is available on all browsers | ||
you want to support, you'll want to include the [webcomponents.js | ||
polyfill](https://github.com/webcomponents/webcomponentsjs) as well: | ||
{ | ||
... | ||
"dependencies": { | ||
"basic-component-mixins": "^0.7", | ||
"webcomponents.js": "^0.7.2" | ||
}, | ||
} | ||
Then issue an `npm install` as usual. | ||
A [sample-component](https://github.com/basic-web-components/sample-component) | ||
project demonstrates the use of npm to depend on the basic-component-mixins | ||
package. It shows the creation of a simple component in both ES6 and ES5. | ||
## ES6 | ||
Your ES6 code can reference the mixin as a function exported by the | ||
corresponding file in this package's /src folder. You then apply the mixin to | ||
the element base class you'd like to use. This can be `HTMLElement`, or an | ||
element class of your own creation. | ||
As a very simple example, if you'd like to create a web component that puts the | ||
word "Hello" before its tag contents, you can use the ShadowTemplate mixin. This | ||
will look for a `template` property on the component, attach a new Shadow DOM | ||
subtree to the component, then copy the template into the shadow subtree. | ||
import ShadowTemplate from 'basic-component-mixins/src/ShadowTemplate'; | ||
// Create a simple custom element that supports a template. | ||
class GreetElement extends ShadowTemplate(HTMLElement) { | ||
get template() { | ||
return ` | ||
Hello, <slot></slot>. | ||
`; | ||
} | ||
} | ||
// Register the custom element with the browser. | ||
document.registerElement('greet-element', GreetElement); | ||
Compile this source with your favorite ES6 processor (e.g., | ||
[Babel](https://babeljs.org)), then load the result into a page. | ||
<html> | ||
<head> | ||
<script src="node_modules/webcomponents.js/webcomponents.js"></script> | ||
<script src="greet-element.js"></script> | ||
</head> | ||
<body> | ||
<!-- Hello, world. --> | ||
<greet-element>world</greet-element> | ||
</body> | ||
</html> | ||
## ES5 | ||
This package's /dist folder contains a JavaScript file that defines all the | ||
mixins as globals available via `window.Basic`. For example, the ShadowTemplate | ||
mixin shown above is available as `window.Basic.ShadowTemplate`. | ||
You can create your custom element class by hand: | ||
// greet-element.js | ||
var GreetElement = Basic.ShadowTemplate(HTMLElement); | ||
GreetElement.prototype.template = 'Hello, <slot></slot>.'; | ||
document.registerElement('greet-element', GreetElement); | ||
You can also use Composable mixin to create the class: | ||
// greet-element.js | ||
var GreetElement = Basic.Composable(HTMLElement).compose( | ||
ShadowTemplate, | ||
{ | ||
template: 'Hello, <slot></slot>.' | ||
} | ||
); | ||
document.registerElement('greet-element', GreetElement); | ||
Then load the script defining the element into your page: | ||
<html> | ||
<head> | ||
<script src="node_modules/webcomponents.js/webcomponents.js"></script> | ||
<script src="node_modules/basic-component-mixins/dist/basic-component-mixins.js"></script> | ||
<script src="greet-element.js"></script> | ||
</head> | ||
<body> | ||
<!-- Hello, world. --> | ||
<greet-element>world</greet-element> | ||
</body> | ||
</html> | ||
# Web component mixins | ||
The /src folder includes mixins for common web component features: | ||
The /src folder includes the complete set of mixins, each of which address some | ||
common web component feature: | ||
@@ -73,2 +266,5 @@ * [AttributeMarshalling](docs/AttributeMarshalling.md). | ||
Facilitates the application of a set of mixins. | ||
* [composeTemplates](docs/composeTemplates.md). | ||
Not a mixin, but a helper function for letting a component insert its template | ||
inside a template defined by a base class. | ||
* [ContentAsItems](docs/ContentAsItems.md). | ||
@@ -131,5 +327,2 @@ Lets a component treat its content as items in a list. | ||
semantics. | ||
* [composeTemplates](docs/composeTemplates.md). | ||
Not a mixin, but a helper function for letting a component insert its template | ||
inside a template defined by a base class. | ||
@@ -136,0 +329,0 @@ |
@@ -1,32 +0,71 @@ | ||
/** | ||
* @class AttributeMarshalling | ||
* @classdesc Mixin which marshalls attributes to properties (and eventually | ||
* vice versa) | ||
* | ||
* This only supports string properties for now. | ||
*/ | ||
/* Exported function extends a base class with AttributeMarshalling. */ | ||
export default (base) => { | ||
/** | ||
* Mixin which marshalls attributes to properties (and eventually vice versa). | ||
* | ||
* If your component exposes a setter for a property, it's generally a good | ||
* idea to let devs using your component be able to set that property in HTML | ||
* via an element attribute. You can code that yourself by writing an | ||
* `attributeChangedCallback`, or you can use this mixin to get a degree of | ||
* automatic support. | ||
* | ||
* This mixin implements an `attributeChangedCallback` that will attempt to | ||
* convert a change in an element attribute into a call to the corresponding | ||
* property setter. Attributes typically follow hyphenated names ("foo-bar"), | ||
* whereas properties typically use camelCase names ("fooBar"). This mixin | ||
* respects that convention, automatically mapping the hyphenated attribute | ||
* name to the corresponding camelCase property name. | ||
* | ||
* Example: You define a component using this mixin: | ||
* | ||
* class MyElement extends AttributeMarshalling(HTMLElement) { | ||
* get fooBar() { return this._fooBar; } | ||
* set fooBar(value) { this._fooBar = value; } | ||
* } | ||
* document.registerElement('my-element', MyElement); | ||
* | ||
* If someone then instantiates your component in HTML: | ||
* | ||
* <my-element foo-bar="Hello"></my-element> | ||
* | ||
* Then, after the element has been upgraded, the `fooBar` setter will | ||
* automatically be invoked with the initial value "Hello". | ||
* | ||
* For the time being, this mixin only supports string-valued properties. | ||
* If you'd like to convert string attributes to other types (numbers, | ||
* booleans), you need to implement `attributeChangedCallback` yourself. | ||
*/ | ||
class AttributeMarshalling extends base { | ||
export default (base) => class AttributeMarshalling extends base { | ||
/* | ||
* Handle a change to the attribute with the given name. | ||
*/ | ||
attributeChangedCallback(name, oldValue, newValue) { | ||
if (super.attributeChangedCallback) { super.attributeChangedCallback(); } | ||
// If the attribute name corresponds to a property name, then set that | ||
// property. Ignore changes in standard HTMLElement properties. | ||
let propertyName = attributeToPropertyName(name); | ||
if (propertyName in this && !(propertyName in HTMLElement.prototype)) { | ||
this[propertyName] = newValue; | ||
} | ||
} | ||
/* | ||
* Handle a change to the attribute with the given name. | ||
*/ | ||
attributeChangedCallback(name, oldValue, newValue) { | ||
if (super.attributeChangedCallback) { super.attributeChangedCallback(); } | ||
// If the attribute name corresponds to a property name, then set that | ||
// property. Ignore changes in standard HTMLElement properties. | ||
let propertyName = attributeToPropertyName(name); | ||
if (propertyName in this && !(propertyName in HTMLElement.prototype)) { | ||
this[propertyName] = newValue; | ||
/* | ||
* Generate an initial call to attributeChangedCallback for each attribute | ||
* on the element. | ||
* | ||
* TODO: The plan for Custom Elements v1 is for the browser to handle this. | ||
* Once that's handled (including in polyfills), this call can go away. | ||
*/ | ||
createdCallback() { | ||
if (super.createdCallback) { super.createdCallback(); } | ||
[].forEach.call(this.attributes, attribute => { | ||
this.attributeChangedCallback(attribute.name, undefined, attribute.value); | ||
}); | ||
} | ||
} | ||
createdCallback() { | ||
if (super.createdCallback) { super.createdCallback(); } | ||
[].forEach.call(this.attributes, attribute => { | ||
this.attributeChangedCallback(attribute.name, undefined, attribute.value); | ||
}); | ||
} | ||
return AttributeMarshalling; | ||
}; | ||
@@ -33,0 +72,0 @@ |
@@ -1,40 +0,49 @@ | ||
/** | ||
* @class ClickSelection | ||
* @classdesc Mixin which maps a click (actually, a mousedown) to selection | ||
* | ||
* If the user clicks an element, and the element is an item in the list, then | ||
* the component's selectedIndex will be set to the index for that item. | ||
*/ | ||
/* Exported function extends a base class with ClickSelection. */ | ||
export default (base) => { | ||
/** | ||
* Mixin which maps a click (actually, a mousedown) to a selection. | ||
* | ||
* This simple mixin is useful in list box-like elements, where a click on a | ||
* list item implicitly selects it. | ||
* | ||
* This mixin expects the component to provide a method `indexOfItem(item)`. | ||
* You can provide that method yourself, or use the ContentAsItems mixin. | ||
* This mixin also expects the component to define a `selectedIndex` | ||
* property. You can provide that yourself, or use the ItemsSelection mixin. | ||
*/ | ||
class ClickSelection extends base { | ||
export default (base) => class ClickSelection extends base { | ||
createdCallback() { | ||
if (super.createdCallback) { super.createdCallback(); } | ||
/* | ||
* REVIEW: Which event should we listen to here? | ||
* | ||
* The standard use for this mixin is in list boxes. List boxes don't | ||
* appear to be consistent with regard to whether they select on mousedown | ||
* or click/mouseup. | ||
*/ | ||
this.addEventListener('mousedown', event => { | ||
selectTarget(this, event.target); | ||
// Note: We don't call preventDefault here. The default behavior for | ||
// mousedown includes setting keyboard focus if the element doesn't | ||
// already have the focus, and we want to preserve that behavior. | ||
event.stopPropagation(); | ||
}); | ||
} | ||
createdCallback() { | ||
if (super.createdCallback) { super.createdCallback(); } | ||
/* | ||
* REVIEW: Which event should we listen to here? | ||
* | ||
* The standard use for this mixin is in list boxes. List boxes don't | ||
* appear to be consistent with regard to whether they select on mousedown | ||
* or click/mouseup. | ||
*/ | ||
this.addEventListener('mousedown', event => { | ||
selectTarget(this, event.target); | ||
// Note: We don't call preventDefault here. The default behavior for | ||
// mousedown includes setting keyboard focus if the element doesn't | ||
// already have the focus, and we want to preserve that behavior. | ||
event.stopPropagation(); | ||
}); | ||
} | ||
// Default implementation. This will typically be handled by other mixins. | ||
get selectedIndex() { | ||
return super.selectedIndex; | ||
} | ||
set selectedIndex(index) { | ||
if ('selectedIndex' in base.prototype) { super.selectedIndex = index; } | ||
} | ||
// Default implementation. This will typically be handled by other mixins. | ||
get selectedIndex() { | ||
return super.selectedIndex; | ||
} | ||
set selectedIndex(index) { | ||
if ('selectedIndex' in base.prototype) { super.selectedIndex = index; } | ||
} | ||
return ClickSelection; | ||
}; | ||
// TODO: Handle the case where a list item has subelements. Walk up the DOM | ||
@@ -41,0 +50,0 @@ // hierarchy until we find an item in the list, or come back to this element, |
/** | ||
* @class Collective | ||
* @classdesc A group of elements that have been joined together for the purpose of | ||
* A group of elements that have been associated for the purpose of | ||
* accomplishing some collective behavior, e.g., keyboard handling. | ||
* | ||
* This is not a mixin, but a class used by the TargetInCollective mixin. | ||
* There are certain components that want to cooperatively handle the keyboard. | ||
* For example, the basic-arrow-selection and basic-page-dots components are | ||
* optional components that can augment the appearance and behavior of an inner | ||
* basic-carousel, adding arrow buttons and dot buttons, respectively. When | ||
* these components are nested together, they form an implicit unit called a | ||
* *collective*: | ||
* | ||
* <basic-arrow-selection> | ||
* <basic-page-dots> | ||
* <basic-carousel> | ||
* ... images, etc. ... | ||
* </basic-carousel> | ||
* </basic-page-dots> | ||
* </basic-arrow-selection> | ||
* | ||
* In this configuration, the three components will all have a `this.collective` | ||
* reference that refers to a shared instance of the `Collective` class. | ||
* | ||
* The Keyboard mixin they use is sensitive to the presence of | ||
* the collective. Among other things, it will ensure that only the outermost | ||
* element above — the basic-arrow-selection — will be a tab stop that can | ||
* receive the keyboard focus. This lets the user perceive the component | ||
* arrangement above as a single unit. The Keyboard mixin will also give each | ||
* element in the collective a chance to process any keyboard events. So, even | ||
* though the basic-arrow-selection element will have the focus, the standard | ||
* keyboard navigation provided by basic-carousel will continue to work. | ||
* | ||
* The SelectionAriaActive component also respects collectives when using the | ||
* `aria-activedescendant` and `role` attributes. Those will be applied to the | ||
* outermost element (basic-arrow-selection, above) so that ARIA can correctly | ||
* understand the arrangement of the elements. | ||
* | ||
* You can put elements into collectives yourself, or you can use the | ||
* TargetInCollective mixin. | ||
*/ | ||
class Collective { | ||
export default class Collective { | ||
/** | ||
* Create a collective. | ||
* | ||
* @param {HTMLELement[]} [elements] - Initial elements to add. | ||
*/ | ||
constructor(...elements) { | ||
/** | ||
* The elements in the collective. | ||
* | ||
* @type {HTMLElement[]} | ||
*/ | ||
this.elements = []; | ||
@@ -17,2 +57,15 @@ elements.forEach(element => this.assimilate(element)); | ||
/** | ||
* Add the indicated target to the collective. | ||
* | ||
* By convention, if two elements wants to participate in a collective, and | ||
* one element is an ancestor of the other in the DOM, the ancestor should | ||
* assimilate the descendant instead of the other way around. | ||
* | ||
* After assimilation, any element in the collective that defines a | ||
* `collectiveChanged` method will have that method invoked. This allows | ||
* the collective's elements to respond to changes in the collective. | ||
* | ||
* @param {(HTMLElement|Collective)} target - The element or collective to add. | ||
*/ | ||
assimilate(target) { | ||
@@ -35,2 +88,8 @@ let collectiveChanged; | ||
/** | ||
* Invoke a method on all elements in the collective. | ||
* | ||
* @param {string} method - The name of the method to invoke on all elements. | ||
* @param {object[]} [args] - The arguments to the method | ||
*/ | ||
invokeMethod(method, ...args) { | ||
@@ -47,2 +106,6 @@ // Invoke from innermost to outermost. | ||
/** | ||
* The outermost element in the collective. | ||
* By convention, this is the first element in the `elements` array. | ||
*/ | ||
get outermostElement() { | ||
@@ -85,1 +148,4 @@ return this.elements[0]; | ||
} | ||
export default Collective; |
@@ -1,44 +0,52 @@ | ||
/** | ||
* @class Composable | ||
* @classdesc Mixin to make a class more easily composable with other mixins | ||
* | ||
* The main contribution is the introduction of a `compose` method that applies | ||
* a set of mixin functions and returns the resulting new class. This sugar | ||
* can make the application of many mixins at once easier to read. | ||
*/ | ||
/* Exported function extends a base class with Composable. */ | ||
export default (base) => { | ||
export default (base) => class Composable extends base { | ||
/** | ||
* Apply a set of mixin functions or mixin objects to the present class and | ||
* return the new class. | ||
* Mixin to make a class more easily composable with other mixins. | ||
* | ||
* A call like | ||
* | ||
* let MyClass = Mixin1(Mixin2(Mixin3(Mixin4(Mixin5(BaseClass))))); | ||
* | ||
* Can be converted to: | ||
* | ||
* let MyClass = Composable(BaseClass).compose( | ||
* Mixin1, | ||
* Mixin2, | ||
* Mixin3, | ||
* Mixin4, | ||
* Mixin5 | ||
* ); | ||
* | ||
* This function can also take mixin objects. A mixin object is just a | ||
* shorthand for a mixin function that creates a new subclass with the given | ||
* members. The mixin object's members are *not* copied directly onto the | ||
* prototype of the base class, as with traditional mixins. | ||
* This mixin contributes a `compose` method that applies a set of mixin | ||
* functions and returns the resulting new class. This sugar can make the | ||
* application of many mixins at once easier to read. | ||
*/ | ||
static compose(...mixins) { | ||
// We create a new subclass for each mixin in turn. The result becomes | ||
// the base class extended by any subsequent mixins. It turns out that | ||
// we can use Array.reduce() to concisely express this, using the current | ||
// object as the seed for reduce(). | ||
return mixins.reduce(composeClass, this); | ||
class Composable extends base { | ||
/** | ||
* Apply a set of mixin functions or mixin objects to the present class and | ||
* return the new class. | ||
* | ||
* Instead of writing: | ||
* | ||
* let MyClass = Mixin1(Mixin2(Mixin3(Mixin4(Mixin5(BaseClass))))); | ||
* | ||
* You can write: | ||
* | ||
* let MyClass = Composable(BaseClass).compose( | ||
* Mixin1, | ||
* Mixin2, | ||
* Mixin3, | ||
* Mixin4, | ||
* Mixin5 | ||
* ); | ||
* | ||
* This function can also take mixin objects. A mixin object is just a | ||
* shorthand for a mixin function that creates a new subclass with the given | ||
* members. The mixin object's members are *not* copied directly onto the | ||
* prototype of the base class, as with traditional mixins. | ||
* | ||
* In addition to providing syntactic sugar, this mixin can be used to | ||
* define a class in ES5, which lacks ES6's `class` keyword. | ||
* | ||
* @param {...mixins} mixins - A set of mixin functions or objects to apply. | ||
*/ | ||
static compose(...mixins) { | ||
// We create a new subclass for each mixin in turn. The result becomes | ||
// the base class extended by any subsequent mixins. It turns out that | ||
// we can use Array.reduce() to concisely express this, using the current | ||
// object as the seed for reduce(). | ||
return mixins.reduce(composeClass, this); | ||
} | ||
} | ||
return Composable; | ||
}; | ||
@@ -45,0 +53,0 @@ |
/** | ||
* @class composeTemplates | ||
* @classdesc Given two templates, this "folds" one inside the other | ||
* @method composeTemplates | ||
* @description Given two templates, this "folds" one inside the other. This is | ||
* is useful for defining a component that wants to fill in slots in the | ||
* template of its base class. | ||
* | ||
* For now, the folding process just entails putting the first inside the | ||
* location of the first <content> node in the second template. | ||
* location of the first <slot> node in the second template. | ||
* | ||
* Example: if the first (sub) template is | ||
* Example: if the first (base) template is | ||
* | ||
* <template> | ||
* Hello, <slot></slot>. | ||
* </template> | ||
* <template> | ||
* <b> | ||
* <slot></slot> | ||
* </b> | ||
* </template> | ||
* | ||
* and the second (base) template is | ||
* and the second (subclass) template is | ||
* | ||
* <template> | ||
* <b> | ||
* <slot></slot> | ||
* </b> | ||
* </template> | ||
* <template> | ||
* Hello, <slot></slot>. | ||
* </template> | ||
* | ||
* Then the returned folded template is | ||
* Then the result of calling `composeTemplates(first, second)` is | ||
* | ||
* <template> | ||
* <b> | ||
* Hello, <slot></slot>. | ||
* </b> | ||
* </template> | ||
* <template> | ||
* <b> | ||
* Hello, <slot></slot>. | ||
* </b> | ||
* </template> | ||
* | ||
* Note that this function is not a mixin, but a helper for creating web | ||
* components. | ||
* | ||
* @param {(HTMLTemplate|string)} baseTemplate - The base class template. | ||
* @param {(HTMLTemplate|string)} subTemplate - The subclass template. | ||
*/ | ||
export default function composeTemplates(baseTemplate, mixinTemplate) { | ||
export default function composeTemplates(baseTemplate, subTemplate) { | ||
if (!baseTemplate) { | ||
// No folding necessary. | ||
return mixinTemplate; | ||
return subTemplate; | ||
} | ||
baseTemplate = makeTemplate(baseTemplate); | ||
mixinTemplate = makeTemplate(mixinTemplate); | ||
subTemplate = makeTemplate(subTemplate); | ||
let baseElement = baseTemplate && baseTemplate.content.cloneNode(true); | ||
let mixinElement = mixinTemplate && mixinTemplate.content.cloneNode(true); | ||
let mixinElement = subTemplate && subTemplate.content.cloneNode(true); | ||
@@ -44,0 +52,0 @@ let folded = document.createElement('template'); |
@@ -1,75 +0,109 @@ | ||
/** | ||
* @class ContentAsItems | ||
* @classdesc Mixin which maps content semantics (children) to list item | ||
* semantics | ||
* | ||
* Items differ from children in several ways: | ||
* | ||
* * They are often referenced via index. | ||
* * They may have a selection state. | ||
* * It's common to do work to initialize the appearance or state of a new item. | ||
* * Auxiliary invisible child elements are filtered out and not counted as | ||
* items. Auxiliary elements include link, script, style, and template | ||
* elements. | ||
*/ | ||
/* Exported function extends a base class with ContentAsItems. */ | ||
export default (base) => { | ||
export default (base) => class ContentAsItems extends base { | ||
applySelection(item, selected) { | ||
if (super.applySelection) { super.applySelection(item, selected); } | ||
item.classList.toggle('selected', selected); | ||
} | ||
contentChanged() { | ||
if (super.contentChanged) { super.contentChanged(); } | ||
this._items = null; | ||
this.itemsChanged(); | ||
} | ||
/** | ||
* Returns the positional index for the indicated item. | ||
* Mixin which maps content semantics (elements) to list item semantics. | ||
* | ||
* Because this acts like a getter, this does not invoke a base implementation. | ||
* This mixin expects a component to provide a `content` property returning a | ||
* raw set of elements. You can provide that yourself, or use the | ||
* `DistributedChildrenAsContent` mixin. | ||
* | ||
* @method indexOfItem | ||
* @param {object} item The item whose index is requested. | ||
* @returns {number} The index of the item, or -1 if not found. | ||
* Items differ from element contents in several ways: | ||
* | ||
* * They are often referenced via index. | ||
* * They may have a selection state. | ||
* * It's common to do work to initialize the appearance or state of a new | ||
* item. | ||
* * Auxiliary invisible child elements are filtered out and not counted as | ||
* items. Auxiliary elements include link, script, style, and template | ||
* elements. This filtering ensures that those auxiliary elements can be | ||
* used in markup inside of a list without being treated as list items. | ||
*/ | ||
indexOfItem(item) { | ||
return this.items.indexOf(item); | ||
} | ||
class ContentAsItems extends base { | ||
// Default implementation does nothing. | ||
itemAdded(item) { | ||
if (super.itemAdded) { super.itemAdded(item); } | ||
} | ||
/** | ||
* Apply the selection state to a single item. | ||
* | ||
* Invoke this method to signal that the selected state of the indicated item | ||
* has changed. By default, this applies a `selected` CSS class if the item | ||
* is selected, and removed it if not selected. | ||
* | ||
* @param {HTMLElement} item - The item whose selection state has changed. | ||
* @param {boolean} selected - True if the item is selected, false if not. | ||
*/ | ||
applySelection(item, selected) { | ||
if (super.applySelection) { super.applySelection(item, selected); } | ||
item.classList.toggle('selected', selected); | ||
} | ||
itemsChanged() { | ||
if (super.itemsChanged) { super.itemsChanged(); } | ||
contentChanged() { | ||
if (super.contentChanged) { super.contentChanged(); } | ||
this._items = null; | ||
this.itemsChanged(); | ||
} | ||
// Perform per-item initialization. | ||
this.items.forEach(item => { | ||
if (!item._itemInitialized) { | ||
this.itemAdded(item); | ||
item._itemInitialized = true; | ||
/** | ||
* Return the positional index for the indicated item. | ||
* | ||
* Because this acts like a getter, this does not invoke a base implementation. | ||
* | ||
* @param {HTMLElement} item The item whose index is requested. | ||
* @returns {number} The index of the item, or -1 if not found. | ||
*/ | ||
indexOfItem(item) { | ||
return this.items.indexOf(item); | ||
} | ||
/** | ||
* This method is invoked whenever a new item is added to the list. | ||
* | ||
* The default implementation of this method does nothing. You can override | ||
* this to perform per-item initialization. | ||
* | ||
* @param {HTMLElement} item - The item that was added. | ||
*/ | ||
itemAdded(item) { | ||
if (super.itemAdded) { super.itemAdded(item); } | ||
} | ||
/** | ||
* The current set of items in the list. See the top-level documentation for | ||
* mixin for a description of how items differ from plain content. | ||
* | ||
* @type {HTMLElement[]} | ||
*/ | ||
get items() { | ||
if (this._items == null) { | ||
this._items = filterAuxiliaryElements(this.content); | ||
} | ||
}); | ||
return this._items; | ||
} | ||
this.dispatchEvent(new CustomEvent('items-changed')); | ||
} | ||
/** | ||
* This method is invoked when the underlying contents change. It is also | ||
* invoked on component initialization – since the items have "changed" from | ||
* being nothing. | ||
*/ | ||
itemsChanged() { | ||
if (super.itemsChanged) { super.itemsChanged(); } | ||
/** | ||
* The current set of items in the list. | ||
* | ||
* @property {object} items | ||
*/ | ||
// TODO: property notifications so elements can bind to this property | ||
get items() { | ||
if (this._items == null) { | ||
this._items = filterAuxiliaryElements(this.content); | ||
// Perform per-item initialization. | ||
this.items.forEach(item => { | ||
if (!item._itemInitialized) { | ||
this.itemAdded(item); | ||
item._itemInitialized = true; | ||
} | ||
}); | ||
this.dispatchEvent(new CustomEvent('items-changed')); | ||
} | ||
return this._items; | ||
/* | ||
* @event items-changed | ||
* | ||
* This event is raised when the set of items changes. | ||
*/ | ||
} | ||
return ContentAsItems; | ||
}; | ||
@@ -76,0 +110,0 @@ |
@@ -1,27 +0,56 @@ | ||
/** | ||
* @class ContentFirstChildTarget | ||
* @classdesc Mixin that defines the target of a component -- the element the | ||
* component is managing or somehow responsible for -- as its first child | ||
*/ | ||
/* Exported function extends a base class with ContentFirstChildTarget. */ | ||
export default (base) => { | ||
/** | ||
* Mixin that defines the target of a component — the element the component is | ||
* managing or somehow responsible for — as its first child. | ||
* | ||
* Some components serve to decorate or modify other elements. A common | ||
* pattern is to have one component wrap another, and have the outer, parent | ||
* component implicitly modify the child. This mixin facilitates this by | ||
* implicitly taking an element's first child as its "target". | ||
* | ||
* Example: | ||
* | ||
* <outer-element> | ||
* <inner-element></inner-element> | ||
* </outer-element> | ||
* | ||
* If `outer-element` uses this mixin, then its `target` property will be | ||
* set to point to the `inner-element`, because that is its first child. | ||
* | ||
* This mixin expects a `content` property that returns the element's content. | ||
* You can implement that yourself, or use the DistributedChildrenAsContent | ||
* mixin. | ||
* | ||
* This mixin can be combined with the TargetInCollective mixin to have a | ||
* component participate in collective keyboard handling. * | ||
*/ | ||
class ContentFirstChildTarget extends base { | ||
export default (base) => class ContentFirstChildTarget extends base { | ||
contentChanged() { | ||
if (super.contentChanged) { super.contentChanged(); } | ||
let content = this.content; | ||
let target = content && content[0]; | ||
if (target) { | ||
this.target = target; | ||
} | ||
} | ||
contentChanged() { | ||
if (super.contentChanged) { super.contentChanged(); } | ||
let content = this.content; | ||
let target = content && content[0]; | ||
if (target) { | ||
this.target = target; | ||
/** | ||
* Gets/sets the current target of the component. | ||
* | ||
* @type {HTMLElement} | ||
*/ | ||
get target() { | ||
return this._target; | ||
} | ||
} | ||
set target(element) { | ||
if ('target' in base.prototype) { super.target = element; } | ||
this._target = element; | ||
} | ||
get target() { | ||
return this._target; | ||
} | ||
set target(element) { | ||
if ('target' in base.prototype) { super.target = element; } | ||
this._target = element; | ||
} | ||
return ContentFirstChildTarget; | ||
}; |
@@ -1,55 +0,61 @@ | ||
/** | ||
* @class DirectionSelection | ||
* @classdesc Mixin which maps direction semantics (goLeft, goRight, etc.) to | ||
* selection semantics (selectPrevious, selectNext, etc.) | ||
*/ | ||
/* Exported function extends a base class with DirectionSelection. */ | ||
export default (base) => { | ||
/** | ||
* Mixin which maps direction semantics (goLeft, goRight, etc.) to selection | ||
* semantics (selectPrevious, selectNext, etc.). | ||
* | ||
* This mixin can be used in conjunction with the KeyboardDirection mixin | ||
* (which maps keyboard events to directions) and a mixin that handles | ||
* selection like ItemsSelection. | ||
*/ | ||
class DirectionSelection extends base { | ||
export default (base) => class DirectionSelection extends base { | ||
goDown() { | ||
if (super.goDown) { super.goDown(); } | ||
return this.selectNext(); | ||
} | ||
goDown() { | ||
if (super.goDown) { super.goDown(); } | ||
return this.selectNext(); | ||
} | ||
goEnd() { | ||
if (super.goEnd) { super.goEnd(); } | ||
return this.selectLast(); | ||
} | ||
goEnd() { | ||
if (super.goEnd) { super.goEnd(); } | ||
return this.selectLast(); | ||
} | ||
goLeft() { | ||
if (super.goLeft) { super.goLeft(); } | ||
return this.selectPrevious(); | ||
} | ||
goLeft() { | ||
if (super.goLeft) { super.goLeft(); } | ||
return this.selectPrevious(); | ||
} | ||
goRight() { | ||
if (super.goRight) { super.goRight(); } | ||
return this.selectNext(); | ||
} | ||
goRight() { | ||
if (super.goRight) { super.goRight(); } | ||
return this.selectNext(); | ||
} | ||
goStart() { | ||
if (super.goStart) { super.goStart(); } | ||
return this.selectFirst(); | ||
} | ||
goStart() { | ||
if (super.goStart) { super.goStart(); } | ||
return this.selectFirst(); | ||
} | ||
goUp() { | ||
if (super.goUp) { super.goUp(); } | ||
return this.selectPrevious(); | ||
} | ||
goUp() { | ||
if (super.goUp) { super.goUp(); } | ||
return this.selectPrevious(); | ||
} | ||
// Default implementations. These will typically be handled by other mixins. | ||
selectFirst() { | ||
if (super.selectFirst) { return super.selectFirst(); } | ||
} | ||
selectLast() { | ||
if (super.selectLast) { return super.selectLast(); } | ||
} | ||
selectNext() { | ||
if (super.selectNext) { return super.selectNext(); } | ||
} | ||
selectPrevious() { | ||
if (super.selectPrevious) { return super.selectPrevious(); } | ||
} | ||
// Default implementations. These will typically be handled by other mixins. | ||
selectFirst() { | ||
if (super.selectFirst) { return super.selectFirst(); } | ||
} | ||
selectLast() { | ||
if (super.selectLast) { return super.selectLast(); } | ||
} | ||
selectNext() { | ||
if (super.selectNext) { return super.selectNext(); } | ||
} | ||
selectPrevious() { | ||
if (super.selectPrevious) { return super.selectPrevious(); } | ||
} | ||
return DirectionSelection; | ||
}; |
@@ -1,41 +0,89 @@ | ||
/** | ||
* @class DistributedChildren | ||
* @classdesc Mixin which defines helpers for accessing a component's | ||
* distributed children as a flattened array or string. | ||
*/ | ||
// TODO: Rationalize with new Custom Elements API. | ||
// TODO: Consider renaming to match Custom Elements API. | ||
export default (base) => class DistributedChildren extends base { | ||
/* Exported function extends a base class with DistributedChildren. */ | ||
export default (base) => { | ||
/* | ||
* Returns an in-order collection of children, expanding any content nodes. | ||
* Like the standard children property, this skips text nodes. | ||
/** | ||
* Mixin which defines helpers for accessing a component's distributed | ||
* children as a flattened array or string. | ||
* | ||
* TODO: This walks the whole content tree every time the list is requested. | ||
* It'd be nice to cache the answer and invalidate it only when content | ||
* actually changes. | ||
* The standard DOM API provides several ways of accessing child content: | ||
* `children`, `childNodes`, and `textContent`. None of these functions are | ||
* Shadow DOM aware. This mixin defines variations of those functions that | ||
* *are* Shadow DOM aware. | ||
* | ||
* Example: you create a component `<count-children>` that displays a number | ||
* equal to the number of children placed inside that component. If someone | ||
* instantiates your component like: | ||
* | ||
* <count-children> | ||
* <div></div> | ||
* <div></div> | ||
* <div></div> | ||
* </count-children> | ||
* | ||
* Then the component should show "3", because there are three children. To | ||
* calculate the number of children, the component can just calculate | ||
* `this.children.length`. However, suppose someone instantiates your | ||
* component inside one of their own components, and puts a `<slot>` element | ||
* inside your component: | ||
* | ||
* <count-children> | ||
* <slot></slot> | ||
* </count-children> | ||
* | ||
* If your component only looks at `this.children`, it will always see exactly | ||
* one child — the `<slot>` element. But the user looking at the page will | ||
* *see* any nodes distributed to that slot. To match what the user sees, your | ||
* component should expand any `<slot>` elements it contains. | ||
* | ||
* That is the problem this mixin solves. After applying this mixin, your | ||
* component code has access to `this.distributedChildren`, whose `length` | ||
* will return the total number of all children distributed to your component | ||
* in the composed tree. | ||
* | ||
* Note: The latest Custom Elements API design calls for a new function, | ||
* `getAssignedNodes` that takes an optional `deep` parameter, that will solve | ||
* this problem at the API level. | ||
*/ | ||
get distributedChildren() { | ||
return expandContentElements(this.children, false); | ||
} | ||
class DistributedChildren extends base { | ||
/* | ||
* Returns an in-order collection of child nodes, expanding any content nodes. | ||
* Like the standard childNodes property, this includes text nodes. | ||
*/ | ||
get distributedChildNodes() { | ||
return expandContentElements(this.childNodes, true); | ||
} | ||
/** | ||
* An in-order collection of children, expanding any slot elements. Like the | ||
* standard children property, this skips text nodes. | ||
* | ||
* @type {HTMLElement[]} | ||
*/ | ||
get distributedChildren() { | ||
return expandContentElements(this.children, false); | ||
} | ||
/* | ||
* Returns the concatenated text content of all child nodes, expanding any | ||
* content nodes. | ||
*/ | ||
get distributedTextContent() { | ||
let strings = this.distributedChildNodes.map(function(child) { | ||
return child.textContent; | ||
}); | ||
return strings.join(''); | ||
/** | ||
* An in-order collection of child nodes, expanding any slot elements. Like | ||
* the standard childNodes property, this includes text nodes. | ||
* | ||
* @type {Node[]} | ||
*/ | ||
get distributedChildNodes() { | ||
return expandContentElements(this.childNodes, true); | ||
} | ||
/** | ||
* The concatenated text content of all child nodes, expanding any slot | ||
* elements. | ||
* | ||
* @type {string} | ||
*/ | ||
get distributedTextContent() { | ||
let strings = this.distributedChildNodes.map(function(child) { | ||
return child.textContent; | ||
}); | ||
return strings.join(''); | ||
} | ||
} | ||
return DistributedChildren; | ||
}; | ||
@@ -42,0 +90,0 @@ |
@@ -1,24 +0,32 @@ | ||
/** | ||
* @class DistributedChildrenAsContent | ||
* @classdesc Mixin which defines a component's content as its children, | ||
* including any nodes distributed to the component's slots. | ||
*/ | ||
/* Exported function extends a base class with DistributedChildrenAsContent. */ | ||
export default (base) => { | ||
export default (base) => class DistributedChildrenAsContent extends base { | ||
/** | ||
* The content of this component, defined to be the flattened array of | ||
* children distributed to the component. | ||
* Mixin which defines a component's content as its children, expanding any | ||
* nodes distributed to the component's slots. | ||
* | ||
* @property content | ||
* @type Array | ||
* This mixin is intended for use with the DistributedChildren mixin. See that | ||
* mixin for a discussion of how that works. This DistributedChildrenAsContent | ||
* mixin provides an easy way of defining the "content" of a component as the | ||
* component's distributed children. That in turn lets mixins like | ||
* ContentAsItems manipulate the children as list items. | ||
*/ | ||
get content() { | ||
return this.distributedChildren; | ||
class DistributedChildrenAsContent extends base { | ||
/** | ||
* The content of this component, defined to be the flattened array of | ||
* children distributed to the component. | ||
* | ||
* @type {HTMLElement[]} | ||
*/ | ||
get content() { | ||
return this.distributedChildren; | ||
} | ||
set content(value) { | ||
if ('content' in base.prototype) { super.content = value; } | ||
} | ||
} | ||
set content(value) { | ||
if ('content' in base.prototype) { super.content = value; } | ||
} | ||
return DistributedChildrenAsContent; | ||
}; |
@@ -1,69 +0,71 @@ | ||
/** | ||
* @class Generic | ||
* @classdesc Mixin that allows a component to support a "generic" style: a | ||
* minimalist style that can easily be removed to reset its visual appearance to | ||
* a baseline state | ||
* | ||
* By default, a component should provide a minimal visual presentation that | ||
* allows the component to function. However, the more styling the component | ||
* provides by default, the harder it becomes to get the component to fit in | ||
* in other settings. Each CSS rule has to be overridden. Worse, new CSS rules | ||
* added to the default style won't be overridden by default, making it hard to | ||
* know whether a new version of a component will still look okay. | ||
* | ||
* As a compromise, the simple Polymer behavior here defines a "generic" | ||
* attribute. This attribute is normally set by default, and styles can be | ||
* written that apply only when the generic attribute is set. This allows the | ||
* construction of CSS rules that will only apply to generic components like | ||
* | ||
* :host([generic=""]) { | ||
* ... | ||
* } | ||
* | ||
* This makes it easy to remove all default styling -- set the generic attribute | ||
* to false, and all default styling will be removed. | ||
*/ | ||
/* Exported function extends a base class with Generic. */ | ||
export default (base) => { | ||
export default (base) => class Generic extends base { | ||
createdCallback() { | ||
if (super.createdCallback) { super.createdCallback(); } | ||
this.generic = this.getAttribute('generic') || true; | ||
} | ||
/** | ||
* True if the component would like to receive generic styling. | ||
* Mixin which allows a component to support a "generic" style: a minimalist | ||
* style that can easily be removed to reset its visual appearance to a | ||
* baseline state. | ||
* | ||
* This property is true by default — set it to false to turn off all | ||
* generic styles. This makes it easier to apply custom styling; you won't | ||
* have to explicitly override styling you don't want. | ||
* By default, a component should provide a minimal visual presentation that | ||
* allows the component to function. However, the more styling the component | ||
* provides by default, the harder it becomes to get the component to fit in | ||
* in other settings. Each CSS rule has to be overridden. Worse, new CSS rules | ||
* added to the default style won't be overridden by default, making it hard | ||
* to know whether a new version of a component will still look okay. | ||
* | ||
* @property generic | ||
* @type Boolean | ||
* @default true | ||
* As a compromise, the mixin defines a `generic` attribute. This attribute is | ||
* normally set by default, and styles can be written that apply only when the | ||
* generic attribute is set. This allows the construction of CSS rules that | ||
* will only apply to generic components like: | ||
* | ||
* :host([generic=""]) { | ||
* ... Generic appearance defined here ... | ||
* } | ||
* | ||
* This makes it easy to remove all default styling — set the `generic` | ||
* attribute to false, and all default styling will be removed. | ||
*/ | ||
get generic() { | ||
return this._generic; | ||
} | ||
set generic(value) { | ||
if ('generic' in base.prototype) { super.generic = value; } | ||
// We roll our own attribute setting so that an explicitly false value shows | ||
// up as generic="false". | ||
if (typeof value === 'string') { | ||
value = (value !== 'false'); | ||
class Generic extends base { | ||
createdCallback() { | ||
if (super.createdCallback) { super.createdCallback(); } | ||
this.generic = this.getAttribute('generic') || true; | ||
} | ||
this._generic = value; | ||
if (value === false) { | ||
// Explicitly use false string. | ||
this.setAttribute('generic', 'false'); | ||
} else if (value == null) { | ||
// Explicitly remove attribute. | ||
this.removeAttribute('generic'); | ||
} else { | ||
// Use the empty string to get attribute to appear with no value. | ||
this.setAttribute('generic', ''); | ||
/** | ||
* True if the component would like to receive generic styling. | ||
* | ||
* This property is true by default — set it to false to turn off all | ||
* generic styles. This makes it easier to apply custom styling; you won't | ||
* have to explicitly override styling you don't want. | ||
* | ||
* @type Boolean | ||
* @default true | ||
*/ | ||
get generic() { | ||
return this._generic; | ||
} | ||
set generic(value) { | ||
if ('generic' in base.prototype) { super.generic = value; } | ||
// We roll our own attribute setting so that an explicitly false value | ||
// shows up as generic="false". | ||
if (typeof value === 'string') { | ||
value = (value !== 'false'); | ||
} | ||
this._generic = value; | ||
if (value === false) { | ||
// Explicitly use false string. | ||
this.setAttribute('generic', 'false'); | ||
} else if (value == null) { | ||
// Explicitly remove attribute. | ||
this.removeAttribute('generic'); | ||
} else { | ||
// Use the empty string to get attribute to appear with no value. | ||
this.setAttribute('generic', ''); | ||
} | ||
} | ||
} | ||
return Generic; | ||
}; |
@@ -1,203 +0,243 @@ | ||
/** | ||
* @class ItemsSelection | ||
* @classdesc Mixin which manages selection semantics for items in a list | ||
*/ | ||
/* Exported function extends a base class with ItemsSelection. */ | ||
export default (base) => { | ||
/** | ||
* Mixin which manages single-selection semantics for items in a list. | ||
* | ||
* This mixin expects a component to provide an `items` array of all elements | ||
* in the list. A standard way to do that with is the ContentAsItems mixin, | ||
* which takes a component's content (typically its distributed children) as | ||
* the set of list items; see that mixin for details. | ||
* | ||
* This mixin tracks a single selected item in the list, and provides means to | ||
* get and set that state by item position (`selectedIndex`) or item identity | ||
* (`selectedItem`). The selection can be moved in the list via the methods | ||
* `selectFirst`, `selectLast`, `selectNext`, and `selectPrevious`. | ||
* | ||
* This mixin does not produce any user-visible effects to represent | ||
* selection. Other mixins, such as SelectionAriaActive, SelectionHighlight | ||
* and SelectionInView, modify the selected item in common ways to let the | ||
* user know a given item is selected or not selected. | ||
*/ | ||
class ItemsSelection extends base { | ||
/** | ||
* Fires when the selectedItem property changes. | ||
* | ||
* @event selected-item-changed | ||
* @param detail.selectedItem The new selected item. | ||
* @param detail.previousItem The previously selected item. | ||
*/ | ||
/** | ||
* Apply the indicate selection state to the item. | ||
* | ||
* The default implementation of this method does nothing. User-visible | ||
* effects will typically be handled by other mixins. | ||
* | ||
* @param {HTMLElement} item - the item being selected/deselected | ||
* @param {boolean} selected - true if the item is selected, false if not | ||
*/ | ||
applySelection(item, selected) { | ||
if (super.applySelection) { super.applySelection(item, selected); } | ||
} | ||
/** | ||
* Fires when the selectedIndex property changes. | ||
* | ||
* @event selected-item-changed | ||
* @param detail.selectedIndex The new selected index. | ||
*/ | ||
/** | ||
* True if the selection can be moved to the next item, false if not (the | ||
* selected item is the last item in the list). | ||
* | ||
* @type {boolean} | ||
*/ | ||
get canSelectNext() { | ||
return this._canSelectNext; | ||
} | ||
set canSelectNext(canSelectNext) { | ||
if ('canSelectNext' in base.prototype) { super.canSelectNext = canSelectNext; } | ||
this._canSelectNext = canSelectNext; | ||
} | ||
export default (base) => class ItemsSelection extends base { | ||
/** | ||
* True if the selection can be moved to the previous item, false if not | ||
* (the selected item is the first one in the list). | ||
* | ||
* @type {boolean} | ||
*/ | ||
get canSelectPrevious() { | ||
return this._canSelectPrevious; | ||
} | ||
set canSelectPrevious(canSelectPrevious) { | ||
if ('canSelectPrevious' in base.prototype) { super.canSelectPrevious = canSelectPrevious; } | ||
this._canSelectPrevious = canSelectPrevious; | ||
} | ||
// Default implementation. This will typically be handled by other mixins. | ||
applySelection(item, selected) { | ||
if (super.applySelection) { super.applySelection(item, selected); } | ||
} | ||
/** | ||
* Handle a new item being added to the list. | ||
* | ||
* The default implementation of this method simply sets the item's | ||
* selection state to false. | ||
* | ||
* @param {HTMLElement} item - the item being added | ||
*/ | ||
itemAdded(item) { | ||
if (super.itemAdded) { super.itemAdded(item); } | ||
this.applySelection(item, item === this.selectedItem); | ||
} | ||
get canSelectNext() { | ||
return this._canSelectNext; | ||
} | ||
set canSelectNext(canSelectNext) { | ||
if ('canSelectNext' in base.prototype) { super.canSelectNext = canSelectNext; } | ||
this._canSelectNext = canSelectNext; | ||
} | ||
itemsChanged() { | ||
if (super.itemsChanged) { super.itemsChanged(); } | ||
let index = this.items.indexOf(this.selectedItem); | ||
if (index < 0) { | ||
// Selected item is no longer in the current set of items. | ||
this.selectedItem = null; | ||
if (this.selectionRequired) { | ||
// Ensure selection, but do this in the next tick to give other | ||
// mixins a chance to do their own itemsChanged work. | ||
setTimeout(function() { | ||
ensureSelection(this); | ||
}.bind(this)); | ||
} | ||
} | ||
get canSelectPrevious() { | ||
return this._canSelectPrevious; | ||
} | ||
set canSelectPrevious(canSelectPrevious) { | ||
if ('canSelectPrevious' in base.prototype) { super.canSelectPrevious = canSelectPrevious; } | ||
this._canSelectPrevious = canSelectPrevious; | ||
} | ||
// The change in items may have affected which navigations are possible. | ||
updatePossibleNavigations(this, index); | ||
} | ||
itemAdded(item) { | ||
if (super.itemAdded) { super.itemAdded(item); } | ||
this.applySelection(item, item === this.selectedItem); | ||
} | ||
/** | ||
* The index of the item which is currently selected, or -1 if there is no | ||
* selection. | ||
* | ||
* Setting the index to -1 deselects any current-selected item. | ||
* | ||
* @type {number} | ||
*/ | ||
get selectedIndex() { | ||
let selectedItem = this.selectedItem; | ||
itemsChanged() { | ||
if (super.itemsChanged) { super.itemsChanged(); } | ||
let index = this.items.indexOf(this.selectedItem); | ||
if (index < 0) { | ||
// Selected item is no longer in the current set of items. | ||
this.selectedItem = null; | ||
if (this.selectionRequired) { | ||
// Ensure selection, but do this in the next tick to give other | ||
// mixins a chance to do their own itemsChanged work. | ||
setTimeout(function() { | ||
ensureSelection(this); | ||
}.bind(this)); | ||
if (selectedItem == null) { | ||
return -1; | ||
} | ||
} | ||
// The change in items may have affected which navigations are possible. | ||
updatePossibleNavigations(this, index); | ||
} | ||
// TODO: Memoize | ||
let index = this.indexOfItem(selectedItem); | ||
/** | ||
* The index of the item which is currently selected, or -1 if there is no | ||
* selection. | ||
* | ||
* @property selectedIndex | ||
* @type Number | ||
*/ | ||
get selectedIndex() { | ||
let selectedItem = this.selectedItem; | ||
// If index = -1, selection wasn't found. Most likely cause is that the | ||
// DOM was manipulated from underneath us. | ||
// TODO: Once we track content changes, turn this into an exception. | ||
return index; | ||
} | ||
set selectedIndex(index) { | ||
if ('selectedIndex' in base.prototype) { super.selectedIndex = index; } | ||
let items = this.items; | ||
let item; | ||
if (index < 0 || items.length === 0) { | ||
item = null; | ||
} else { | ||
item = items[index]; | ||
} | ||
this.selectedItem = item; | ||
if (selectedItem == null) { | ||
return -1; | ||
let event = new CustomEvent('selected-index-changed', { | ||
detail: { | ||
selectedIndex: index, | ||
value: index // for Polymer binding | ||
} | ||
}); | ||
this.dispatchEvent(event); | ||
} | ||
// TODO: Memoize | ||
let index = this.indexOfItem(selectedItem); | ||
// If index = -1, selection wasn't found. Most likely cause is that the | ||
// DOM was manipulated from underneath us. | ||
// TODO: Once we track content changes, turn this into an exception. | ||
return index; | ||
} | ||
set selectedIndex(index) { | ||
if ('selectedIndex' in base.prototype) { super.selectedIndex = index; } | ||
let items = this.items; | ||
let item; | ||
if (index < 0 || items.length === 0) { | ||
item = null; | ||
} else { | ||
item = items[index]; | ||
/** | ||
* The currently selected item, or null if there is no selection. | ||
* | ||
* Setting this property to null deselects any currently-selected item. | ||
* | ||
* @type {object} | ||
*/ | ||
get selectedItem() { | ||
return this._selectedItem || null; | ||
} | ||
this.selectedItem = item; | ||
set selectedItem(item) { | ||
if ('selectedItem' in base.prototype) { super.selectedItem = item; } | ||
let previousItem = this._selectedItem; | ||
if (previousItem) { | ||
// Remove previous selection. | ||
this.applySelection(previousItem, false); | ||
} | ||
let event = new CustomEvent('selected-index-changed', { | ||
detail: { | ||
selectedIndex: index, | ||
value: index // for Polymer binding | ||
// TODO: Confirm item is actually in the list before selecting. | ||
this._selectedItem = item; | ||
if (item) { | ||
this.applySelection(item, true); | ||
} | ||
}); | ||
this.dispatchEvent(event); | ||
} | ||
/** | ||
* The currently selected item, or null if there is no selection. | ||
* | ||
* @property selectedItem | ||
* @type Object | ||
*/ | ||
// TODO: Confirm item is in items before selecting. | ||
get selectedItem() { | ||
return this._selectedItem || null; | ||
} | ||
set selectedItem(item) { | ||
if ('selectedItem' in base.prototype) { super.selectedItem = item; } | ||
let previousItem = this._selectedItem; | ||
if (previousItem) { | ||
// Remove previous selection. | ||
this.applySelection(previousItem, false); | ||
// TODO: Rationalize with selectedIndex so we're not recalculating item | ||
// or index in each setter. | ||
let index = this.indexOfItem(item); | ||
updatePossibleNavigations(this, index); | ||
let event = new CustomEvent('selected-item-changed', { | ||
detail: { | ||
selectedItem: item, | ||
previousItem: previousItem, | ||
value: item // for Polymer binding | ||
} | ||
}); | ||
this.dispatchEvent(event); | ||
} | ||
this._selectedItem = item; | ||
if (item) { | ||
this.applySelection(item, true); | ||
/** | ||
* Select the first item in the list. | ||
*/ | ||
selectFirst() { | ||
if (super.selectFirst) { super.selectFirst(); } | ||
return selectIndex(this, 0); | ||
} | ||
// TODO: Rationalize with selectedIndex so we're not recalculating item | ||
// or index in each setter. | ||
let index = this.indexOfItem(item); | ||
updatePossibleNavigations(this, index); | ||
/** | ||
* True if the list should always have a selection (if it has items). | ||
* | ||
* @type {boolean} | ||
*/ | ||
get selectionRequired() { | ||
return this._selectionRequired; | ||
} | ||
set selectionRequired(selectionRequired) { | ||
if ('selectionRequired' in base.prototype) { super.selectionRequired = selectionRequired; } | ||
this._selectionRequired = selectionRequired; | ||
ensureSelection(this); | ||
} | ||
let event = new CustomEvent('selected-item-changed', { | ||
detail: { | ||
selectedItem: item, | ||
previousItem: previousItem, | ||
value: item // for Polymer binding | ||
} | ||
}); | ||
this.dispatchEvent(event); | ||
} | ||
/** | ||
* Select the last item in the list. | ||
*/ | ||
selectLast() { | ||
if (super.selectLast) { super.selectLast(); } | ||
return selectIndex(this, this.items.length - 1); | ||
} | ||
/** | ||
* Select the first item in the list. | ||
* | ||
* @method selectFirst | ||
*/ | ||
selectFirst() { | ||
if (super.selectFirst) { super.selectFirst(); } | ||
return selectIndex(this, 0); | ||
} | ||
/** | ||
* Select the next item in the list. | ||
*/ | ||
selectNext() { | ||
if (super.selectNext) { super.selectNext(); } | ||
return selectIndex(this, this.selectedIndex + 1); | ||
} | ||
/** | ||
* True if the list should always have a selection (if it has items). | ||
* | ||
* @property selectionRequired | ||
* @type Boolean | ||
*/ | ||
get selectionRequired() { | ||
return this._selectionRequired; | ||
} | ||
set selectionRequired(selectionRequired) { | ||
if ('selectionRequired' in base.prototype) { super.selectionRequired = selectionRequired; } | ||
this._selectionRequired = selectionRequired; | ||
ensureSelection(this); | ||
} | ||
/** | ||
* Select the previous item in the list. | ||
*/ | ||
selectPrevious() { | ||
if (super.selectPrevious) { super.selectPrevious(); } | ||
return selectIndex(this, this.selectedIndex - 1); | ||
} | ||
/** | ||
* Select the last item in the list. | ||
* | ||
* @method selectLast | ||
*/ | ||
selectLast() { | ||
if (super.selectLast) { super.selectLast(); } | ||
return selectIndex(this, this.items.length - 1); | ||
} | ||
/** | ||
* Fires when the selectedItem property changes. | ||
* | ||
* @event selected-item-changed | ||
* @param {HTMLElement} detail.selectedItem The new selected item. | ||
* @param {HTMLElement} detail.previousItem The previously selected item. | ||
*/ | ||
/** | ||
* Select the next item in the list. | ||
* | ||
* @method selectNext | ||
*/ | ||
selectNext() { | ||
if (super.selectNext) { super.selectNext(); } | ||
return selectIndex(this, this.selectedIndex + 1); | ||
} | ||
/** | ||
* Fires when the selectedIndex property changes. | ||
* | ||
* @event selected-item-changed | ||
* @param {number} detail.selectedIndex The new selected index. | ||
*/ | ||
/** | ||
* Select the previous item in the list. | ||
* | ||
* @method selectPrevious | ||
*/ | ||
selectPrevious() { | ||
if (super.selectPrevious) { super.selectPrevious(); } | ||
return selectIndex(this, this.selectedIndex - 1); | ||
} | ||
return ItemsSelection; | ||
}; | ||
@@ -204,0 +244,0 @@ |
@@ -1,45 +0,97 @@ | ||
/** | ||
* @class Keyboard | ||
* @classdesc Mixin which manages the keydown handling for a component | ||
* | ||
* TODO: Document collective behavior. | ||
* TODO: Provide baseline behavior outside of a collective. | ||
*/ | ||
// TODO: Provide baseline behavior for this mixin when used outside of a | ||
// collective. | ||
export default (base) => class Keyboard extends base { | ||
/* Exported function extends a base class with Keyboard. */ | ||
export default (base) => { | ||
// Default keydown handler. This will typically be handled by other mixins. | ||
keydown(event) { | ||
if (super.keydown) { return super.keydown(event); } | ||
} | ||
/* | ||
* If we're now the outermost element of the collective, set up to receive | ||
* keyboard events. If we're no longer the outermost element, stop listening. | ||
/** | ||
* Mixin which manages the keydown handling for a component. | ||
* | ||
* This mixin handles several keyboard-related features. | ||
* | ||
* First, it wires up a single keydown event handler that can be shared by | ||
* multiple mixins on a component. The event handler will invoke a `keydown` | ||
* method with the event object, and any mixin along the prototype chain that | ||
* wants to handle that method can do so. | ||
* | ||
* If a mixin wants to indicate that keyboard event has been handled, and that | ||
* other mixins should *not* handle it, the mixin's `keydown` handler should | ||
* return a value of true. The convention that seems to work well is that a | ||
* mixin should see if it wants to handle the event and, if not, then ask the | ||
* superclass to see if it wants to handle the event. This has the effect of | ||
* giving the mixin that was applied last the first chance at handling a | ||
* keyboard event. | ||
* | ||
* Example: | ||
* | ||
* keydown(event) { | ||
* let handled; | ||
* switch (event.keyCode) { | ||
* // Handle the keys you want, setting handled = true if appropriate. | ||
* } | ||
* // Prefer mixin result if it's defined, otherwise use base result. | ||
* return handled || (super.keydown && super.keydown(event)); | ||
* } | ||
* | ||
* A second feature provided by this mixin is that it implicitly makes the | ||
* component a tab stop if it isn't already, by setting `tabIndex` to 0. This | ||
* has the effect of adding the component to the tab order in document order. | ||
* | ||
* Finally, this mixin is designed to work with Collective class via a mixin | ||
* like TargetInCollective. This allows a set of related component instances | ||
* to cooperatively handle the keyboard. See the Collective class for details. | ||
* | ||
* NOTE: For the time being, this mixin should be used with | ||
* TargetInCollective. The intention is to allow this mixin to be used without | ||
* requiring collective keyboard support, so that this mixin can be used on | ||
* its own. | ||
*/ | ||
collectiveChanged() { | ||
if (super.collectiveChanged) { super.collectiveChanged(); } | ||
class Keyboard extends base { | ||
if (this.collective.outermostElement !== this) { | ||
// We're no longer the outermost element; stop listening. | ||
if (isListeningToKeydown(this)) { | ||
stopListeningToKeydown(this); | ||
} | ||
return; | ||
/** | ||
* Handle the indicated keyboard event. | ||
* | ||
* The default implementation of this method does nothing. This will | ||
* typically be handled by other mixins. | ||
* | ||
* @param {KeyboardEvent} event - the keyboard event | ||
* @return {boolean} true if the event was handled | ||
*/ | ||
keydown(event) { | ||
if (super.keydown) { return super.keydown(event); } | ||
} | ||
if (!this.getAttribute('aria-label')) { | ||
// Since we're going to handle the keyboard, see if we can adopt an ARIA | ||
// label from an inner element of the collective. | ||
let label = getCollectiveAriaLabel(this.collective); | ||
if (label) { | ||
this.setAttribute('aria-label', label); | ||
/* | ||
* If we're now the outermost element of the collective, set up to receive | ||
* keyboard events. If we're no longer the outermost element, stop | ||
* listening. | ||
*/ | ||
collectiveChanged() { | ||
if (super.collectiveChanged) { super.collectiveChanged(); } | ||
if (this.collective.outermostElement !== this) { | ||
// We're no longer the outermost element; stop listening. | ||
if (isListeningToKeydown(this)) { | ||
stopListeningToKeydown(this); | ||
} | ||
return; | ||
} | ||
if (!this.getAttribute('aria-label')) { | ||
// Since we're going to handle the keyboard, see if we can adopt an ARIA | ||
// label from an inner element of the collective. | ||
let label = getCollectiveAriaLabel(this.collective); | ||
if (label) { | ||
this.setAttribute('aria-label', label); | ||
} | ||
} | ||
if (!isListeningToKeydown(this)) { | ||
startListeningToKeydown(this); | ||
} | ||
} | ||
if (!isListeningToKeydown(this)) { | ||
startListeningToKeydown(this); | ||
} | ||
} | ||
return Keyboard; | ||
}; | ||
@@ -46,0 +98,0 @@ |
@@ -1,62 +0,102 @@ | ||
/** | ||
* @class KeyboardDirection | ||
* @classdesc Mixin which maps direction keys (Left, Right, etc.) to direction | ||
* semantics (goLeft, goRight, etc.) | ||
*/ | ||
/* Exported function extends a base class with KeyboardDirection. */ | ||
export default (base) => { | ||
/** | ||
* Mixin which maps direction keys (Left, Right, etc.) to direction semantics | ||
* (go left, go right, etc.). | ||
* | ||
* This mixin expects the component to invoke a `keydown` method when a key is | ||
* pressed. You can use the Keyboard mixin for that purpose, or wire up your | ||
* own keyboard handling and call `keydown` yourself. | ||
* | ||
* This mixin calls methods such as `goLeft` and `goRight`. You can define | ||
* what that means by implementing those methods yourself. If you want to use | ||
* direction keys to navigate a selection, use this mixin with the | ||
* DirectionSelection mixin. | ||
*/ | ||
class KeyboardDirection extends base { | ||
export default (base) => class KeyboardDirection extends base { | ||
/** | ||
* Invoked when the user wants to go/navigate down. | ||
* The default implementation of this method does nothing. | ||
*/ | ||
goDown() { | ||
if (super.goDown) { return super.goDown(); } | ||
} | ||
// Default implementations. These will typically be handled by other mixins. | ||
goDown() { | ||
if (super.goDown) { return super.goDown(); } | ||
} | ||
goEnd() { | ||
if (super.goEnd) { return super.goEnd(); } | ||
} | ||
goLeft() { | ||
if (super.goLeft) { return super.goLeft(); } | ||
} | ||
goRight() { | ||
if (super.goRight) { return super.goRight(); } | ||
} | ||
goStart() { | ||
if (super.goStart) { return super.goStart(); } | ||
} | ||
goUp() { | ||
if (super.goUp) { return super.goUp(); } | ||
} | ||
/** | ||
* Invoked when the user wants to go/navigate to the end (e.g., of a list). | ||
* The default implementation of this method does nothing. | ||
*/ | ||
goEnd() { | ||
if (super.goEnd) { return super.goEnd(); } | ||
} | ||
keydown(event) { | ||
let handled; | ||
// Ignore Left/Right keys when metaKey or altKey modifier is also pressed, | ||
// as the user may be trying to navigate back or forward in the browser. | ||
switch (event.keyCode) { | ||
case 35: // End | ||
handled = this.goEnd(); | ||
break; | ||
case 36: // Home | ||
handled = this.goStart(); | ||
break; | ||
case 37: // Left | ||
if (!event.metaKey && !event.altKey) { | ||
handled = this.goLeft(); | ||
} | ||
break; | ||
case 38: // Up | ||
handled = event.altKey ? this.goStart() : this.goUp(); | ||
break; | ||
case 39: // Right | ||
if (!event.metaKey && !event.altKey) { | ||
handled = this.goRight(); | ||
} | ||
break; | ||
case 40: // Down | ||
handled = event.altKey ? this.goEnd() : this.goDown(); | ||
break; | ||
/** | ||
* Invoked when the user wants to go/navigate left. | ||
* The default implementation of this method does nothing. | ||
*/ | ||
goLeft() { | ||
if (super.goLeft) { return super.goLeft(); } | ||
} | ||
// Prefer mixin result if it's defined, otherwise use base result. | ||
return handled || (super.keydown && super.keydown(event)); | ||
/** | ||
* Invoked when the user wants to go/navigate right. | ||
* The default implementation of this method does nothing. | ||
*/ | ||
goRight() { | ||
if (super.goRight) { return super.goRight(); } | ||
} | ||
/** | ||
* Invoked when the user wants to go/navigate to the start (e.g., of a | ||
* list). The default implementation of this method does nothing. | ||
*/ | ||
goStart() { | ||
if (super.goStart) { return super.goStart(); } | ||
} | ||
/** | ||
* Invoked when the user wants to go/navigate up. | ||
* The default implementation of this method does nothing. | ||
*/ | ||
goUp() { | ||
if (super.goUp) { return super.goUp(); } | ||
} | ||
keydown(event) { | ||
let handled; | ||
// Ignore Left/Right keys when metaKey or altKey modifier is also pressed, | ||
// as the user may be trying to navigate back or forward in the browser. | ||
switch (event.keyCode) { | ||
case 35: // End | ||
handled = this.goEnd(); | ||
break; | ||
case 36: // Home | ||
handled = this.goStart(); | ||
break; | ||
case 37: // Left | ||
if (!event.metaKey && !event.altKey) { | ||
handled = this.goLeft(); | ||
} | ||
break; | ||
case 38: // Up | ||
handled = event.altKey ? this.goStart() : this.goUp(); | ||
break; | ||
case 39: // Right | ||
if (!event.metaKey && !event.altKey) { | ||
handled = this.goRight(); | ||
} | ||
break; | ||
case 40: // Down | ||
handled = event.altKey ? this.goEnd() : this.goDown(); | ||
break; | ||
} | ||
// Prefer mixin result if it's defined, otherwise use base result. | ||
return handled || (super.keydown && super.keydown(event)); | ||
} | ||
} | ||
return KeyboardDirection; | ||
}; |
@@ -1,72 +0,75 @@ | ||
/** | ||
* @class KeyboardPagedSelection | ||
* @classdesc Mixin which maps page keys (Page Up, Page Down) into operations | ||
* that move the selection by one page | ||
* | ||
* The keyboard interaction model generally follows that of Microsoft Windows' | ||
* list boxes instead of those in OS X: | ||
* | ||
* * The Page Up/Down and Home/End keys actually change the selection, rather | ||
* than just scrolling. The former behavior seems more generally useful for | ||
* keyboard users. | ||
* | ||
* * Pressing Page Up/Down will change the selection to the topmost/bottommost | ||
* visible item if the selection is not already there. Thereafter, the key | ||
* will move the selection up/down by a page, and (per the above point) make | ||
* the selected item visible. | ||
* | ||
* To ensure the selected item is in view following use of Page Up/Down, use the | ||
* related SelectionInView mixin. | ||
*/ | ||
/* Exported function extends a base class with KeyboardPagedSelection. */ | ||
export default (base) => { | ||
/** | ||
* Mixin which maps page keys (Page Up, Page Down) into operations that move | ||
* the selection by one page. | ||
* | ||
* The keyboard interaction model generally follows that of Microsoft Windows' | ||
* list boxes instead of those in OS X: | ||
* | ||
* * The Page Up/Down and Home/End keys actually change the selection, rather | ||
* than just scrolling. The former behavior seems more generally useful for | ||
* keyboard users. | ||
* | ||
* * Pressing Page Up/Down will change the selection to the topmost/bottommost | ||
* visible item if the selection is not already there. Thereafter, the key | ||
* will move the selection up/down by a page, and (per the above point) make | ||
* the selected item visible. | ||
* | ||
* To ensure the selected item is in view following use of Page Up/Down, use | ||
* the related SelectionInView mixin. | ||
* | ||
* This mixin expects the component to invoke a `keydown` method when a key is | ||
* pressed. You can use the Keyboard mixin for that purpose, or wire up your | ||
* own keyboard handling and call `keydown` yourself. | ||
*/ | ||
class KeyboardPagedSelection extends base { | ||
export default (base) => class KeyboardPagedSelection extends base { | ||
keydown(event) { | ||
let handled; | ||
switch (event.keyCode) { | ||
case 33: // Page Up | ||
handled = this.pageUp(); | ||
break; | ||
case 34: // Page Down | ||
handled = this.pageDown(); | ||
break; | ||
} | ||
// Prefer mixin result if it's defined, otherwise use base result. | ||
return handled || (super.keydown && super.keydown(event)); | ||
} | ||
keydown(event) { | ||
let handled; | ||
switch (event.keyCode) { | ||
case 33: // Page Up | ||
handled = this.pageUp(); | ||
break; | ||
case 34: // Page Down | ||
handled = this.pageDown(); | ||
break; | ||
/** | ||
* Scroll down one page. | ||
*/ | ||
pageDown() { | ||
if (super.pageDown) { super.pageDown(); } | ||
return scrollOnePage(this, true); | ||
} | ||
// Prefer mixin result if it's defined, otherwise use base result. | ||
return handled || (super.keydown && super.keydown(event)); | ||
} | ||
/** | ||
* Scroll down one page. | ||
* | ||
* @method pageDown | ||
*/ | ||
pageDown() { | ||
if (super.pageDown) { super.pageDown(); } | ||
return scrollOnePage(this, true); | ||
} | ||
/** | ||
* Scroll up one page. | ||
*/ | ||
pageUp() { | ||
if (super.pageUp) { super.pageUp(); } | ||
return scrollOnePage(this, false); | ||
} | ||
/** | ||
* Scroll up one page. | ||
* | ||
* @method pageUp | ||
*/ | ||
pageUp() { | ||
if (super.pageUp) { super.pageUp(); } | ||
return scrollOnePage(this, false); | ||
/** | ||
* The element that should be scrolled with the Page Up/Down keys. | ||
* Default is the current element. | ||
* | ||
* @type {HTMLElement} | ||
*/ | ||
get scrollTarget() { | ||
// Prefer base result. | ||
return 'scrollTarget' in base.prototype ? super.scrollTarget : this; | ||
} | ||
set scrollTarget(element) { | ||
if ('scrollTarget' in base.prototype) { super.scrollTarget = element; } | ||
} | ||
} | ||
/** | ||
* The element that should be scrolled with the Page Up/Down keys. | ||
* Default is the current element. | ||
* | ||
* @property scrollTarget | ||
*/ | ||
get scrollTarget() { | ||
// Prefer base result. | ||
return 'scrollTarget' in base.prototype ? super.scrollTarget : this; | ||
} | ||
set scrollTarget(element) { | ||
if ('scrollTarget' in base.prototype) { super.scrollTarget = element; } | ||
} | ||
return KeyboardPagedSelection; | ||
}; | ||
@@ -73,0 +76,0 @@ |
@@ -1,64 +0,99 @@ | ||
/** | ||
* @class KeyboardPrefixSelection | ||
* @classdesc Mixin that handles list box-style prefix typing, in which the user | ||
* can type a string to select the first item that begins with that string | ||
*/ | ||
// TODO: If the selection is changed by some other means (e.g., arrow keys) | ||
// other than prefix typing, then that act should reset the prefix. | ||
/* Exported function extends a base class with KeyboardPrefixSelection. */ | ||
export default (base) => { | ||
// TODO: If the selection is changed by some other means (e.g., arrow keys) other | ||
// than prefix typing, then that act should reset the prefix. | ||
/** | ||
* Mixin that handles list box-style prefix typing, in which the user can type | ||
* a string to select the first item that begins with that string. | ||
* | ||
* Example: suppose a component using this mixin has the following items: | ||
* | ||
* <sample-list-component> | ||
* <div>Apple</div> | ||
* <div>Apricot</div> | ||
* <div>Banana</div> | ||
* <div>Blackberry</div> | ||
* <div>Blueberry</div> | ||
* <div>Cantaloupe</div> | ||
* <div>Cherry</div> | ||
* <div>Lemon</div> | ||
* <div>Lime</div> | ||
* </sample-list-component> | ||
* | ||
* If this component receives the focus, and the user presses the "b" or "B" | ||
* key, the "Banana" item will be selected, because it's the first item that | ||
* matches the prefix "b". (Matching is case-insensitive.) If the user now | ||
* presses the "l" or "L" key quickly, the prefix to match becomes "bl", so | ||
* "Blackberry" will be selected. | ||
* | ||
* The prefix typing feature has a one second timeout — the prefix to match | ||
* will be reset after a second has passed since the user last typed a key. | ||
* If, in the above example, the user waits a second between typing "b" and | ||
* "l", the prefix will become "l", so "Lemon" would be selected. | ||
* | ||
* This mixin expects the component to invoke a `keydown` method when a key is | ||
* pressed. You can use the Keyboard mixin for that purpose, or wire up your | ||
* own keyboard handling and call `keydown` yourself. | ||
* | ||
* This mixin also expects the component to provide an `items` property. The | ||
* `textContent` of those items will be used for purposes of prefix matching. | ||
*/ | ||
class KeyboardPrefixSelection extends base { | ||
export default (base) => class KeyboardPrefixSelection extends base { | ||
// TODO: If the set of items is changed, reset the prefix. | ||
// itemsChanged() { | ||
// this._itemTextContents = null; | ||
// resetTypedPrefix(this); | ||
// } | ||
// itemsChanged() { | ||
// this._itemTextContents = null; | ||
// resetTypedPrefix(this); | ||
// } | ||
keydown(event) { | ||
let handled; | ||
let resetPrefix = true; | ||
keydown(event) { | ||
let handled; | ||
let resetPrefix = true; | ||
switch (event.keyCode) { | ||
case 8: // Backspace | ||
handleBackspace(this); | ||
handled = true; | ||
resetPrefix = false; | ||
break; | ||
case 27: // Escape | ||
handled = true; | ||
break; | ||
default: | ||
if (!event.ctrlKey && !event.metaKey && !event.altKey && | ||
event.which !== 32 /* Space */) { | ||
handlePlainCharacter(this, String.fromCharCode(event.which)); | ||
} | ||
resetPrefix = false; | ||
} | ||
switch (event.keyCode) { | ||
case 8: // Backspace | ||
handleBackspace(this); | ||
handled = true; | ||
resetPrefix = false; | ||
break; | ||
case 27: // Escape | ||
handled = true; | ||
break; | ||
default: | ||
if (!event.ctrlKey && !event.metaKey && !event.altKey && | ||
event.which !== 32 /* Space */) { | ||
handlePlainCharacter(this, String.fromCharCode(event.which)); | ||
} | ||
resetPrefix = false; | ||
if (resetPrefix) { | ||
resetTypedPrefix(this); | ||
} | ||
// Prefer mixin result if it's defined, otherwise use base result. | ||
return handled || (super.keydown && super.keydown(event)); | ||
} | ||
if (resetPrefix) { | ||
resetTypedPrefix(this); | ||
/** | ||
* Select the first item whose text content begins with the given prefix. | ||
* | ||
* @param prefix [String] The prefix string to search for | ||
*/ | ||
selectItemWithTextPrefix(prefix) { | ||
if (super.selectItemWithTextPrefix) { super.selectItemWithTextPrefix(prefix); } | ||
if (prefix == null || prefix.length === 0) { | ||
return; | ||
} | ||
let index = getIndexOfItemWithTextPrefix(this, prefix); | ||
if (index >= 0) { | ||
this.selectedIndex = index; | ||
} | ||
} | ||
// Prefer mixin result if it's defined, otherwise use base result. | ||
return handled || (super.keydown && super.keydown(event)); | ||
} | ||
/** | ||
* Select the first item whose text content begins with the given prefix. | ||
* | ||
* @method selectItemWithTextPrefix | ||
* @param prefix [String] The string to search for | ||
*/ | ||
selectItemWithTextPrefix(prefix) { | ||
if (super.selectItemWithTextPrefix) { super.selectItemWithTextPrefix(prefix); } | ||
if (prefix == null || prefix.length === 0) { | ||
return; | ||
} | ||
let index = getIndexOfItemWithTextPrefix(this, prefix); | ||
if (index >= 0) { | ||
this.selectedIndex = index; | ||
} | ||
} | ||
return KeyboardPrefixSelection; | ||
}; | ||
@@ -65,0 +100,0 @@ |
@@ -27,5 +27,11 @@ /* | ||
/** | ||
* Add a callback to the microtask queue. | ||
* | ||
* This uses a MutationObserver so that it works on IE 11. | ||
* | ||
* NOTE: IE 11 may actually use timeout timing with MutationObservers. This | ||
* needs more investigation. | ||
* | ||
* @function microtask | ||
* | ||
* Adds a function to the microtask queue. | ||
* @param {function} callback | ||
*/ | ||
@@ -32,0 +38,0 @@ export default function microtask(callback) { |
@@ -1,47 +0,68 @@ | ||
/** | ||
* @class ObserveContentChanges | ||
* @classdesc Wires up mutation observers to report any changes in a component's | ||
* content (direct children, or nodes distributed to slots). | ||
* | ||
* For the time being, this can only support a single level of distributed | ||
* content. That is, if a component contains a slot, and the set of nodes | ||
* directly assigned to that slot changes, then this mixin will detect the | ||
* change. However, this mixin does not yet detect changes in reprojected | ||
* nodes. If a component's host places a slot as a child of the component | ||
* instance, nodes assigned to the outer host will be assigned to the host's | ||
* slot, then reassigned to the slot element inside the component. Changes in | ||
* such reprojected nodes will not (yet) be detected by this mixin. | ||
* | ||
* For comparison, see Polymer's observeNodes API, which does solve the problem | ||
* of tracking changes in reprojected content. | ||
*/ | ||
import microtask from './microtask'; | ||
// TODO: Should this be renamed to something like DistributedChildrenChanged? | ||
import microtask from './microtask'; | ||
/* Exported function extends a base class with ObserveContentChanges. */ | ||
export default (base) => { | ||
/** | ||
* Mixin which wires up mutation observers to report any changes in a | ||
* component's content (direct children, or nodes distributed to slots). | ||
* | ||
* For the time being, this can only support a single level of distributed | ||
* content. That is, if a component contains a slot, and the set of nodes | ||
* directly assigned to that slot changes, then this mixin will detect the | ||
* change. However, this mixin does not yet detect changes in reprojected | ||
* nodes. If a component's host places a slot as a child of the component | ||
* instance, nodes assigned to the outer host will be assigned to the host's | ||
* slot, then reassigned to the slot element inside the component. Changes in | ||
* such reprojected nodes will not (yet) be detected by this mixin. | ||
* | ||
* For comparison, see Polymer's observeNodes API, which does solve the | ||
* problem of tracking changes in reprojected content. | ||
* | ||
* Note: The web platform team creating the specifications for web components | ||
* plan to request that a new type of MutationObserver option be defined that | ||
* lets a component monitor changes in distributed children. This mixin will | ||
* be updated to take advantage of that MutationObserver option when that | ||
* becomes available. | ||
*/ | ||
class ObserveContentChanges extends base { | ||
// TODO: Don't respond to changes in attributes, or at least offer that as an | ||
// option. | ||
createdCallback() { | ||
if (super.createdCallback) { super.createdCallback(); } | ||
observeContentChanges(this); | ||
export default (base) => class ObserveContentChanges extends base { | ||
// Make an initial call to contentChanged() so that the component can do | ||
// initialization that it normally does when content changes. | ||
// | ||
// This will invoke contentChanged() handlers in other mixins. In order | ||
// that those mixins have a chance to complete their own initialization, | ||
// we add the contentChanged() call to the microtask queue. | ||
microtask(() => this.contentChanged()); | ||
} | ||
createdCallback() { | ||
if (super.createdCallback) { super.createdCallback(); } | ||
observeContentChanges(this); | ||
/** | ||
* Invoked when the contents of the component (including distributed | ||
* children) have changed. | ||
* | ||
* This method is also invoked when a component is first instantiated; the | ||
* contents have essentially "changed" from being nothing. This allows the | ||
* component to perform initial processing of its children. | ||
*/ | ||
contentChanged() { | ||
if (super.contentChanged) { super.contentChanged(); } | ||
let event = new CustomEvent('content-changed'); | ||
this.dispatchEvent(event); | ||
} | ||
// Make an initial call to contentChanged() so that the component can do | ||
// initialization that it normally does when content changes. | ||
// | ||
// This will invoke contentChanged() handlers in other mixins. In order that | ||
// those mixins have a chance to complete their own initialization, we add | ||
// the contentChanged() call to the microtask queue. | ||
microtask(() => this.contentChanged()); | ||
/** | ||
* This event is raised when the component's contents (including distributed | ||
* children) have changed. | ||
* | ||
* @event content-changed | ||
*/ | ||
} | ||
contentChanged() { | ||
if (super.contentChanged) { super.contentChanged(); } | ||
let event = new CustomEvent('content-changed'); | ||
this.dispatchEvent(event); | ||
} | ||
return ObserveContentChanges; | ||
}; | ||
@@ -48,0 +69,0 @@ |
@@ -1,16 +0,1 @@ | ||
/** | ||
* @class SelectionAriaActive | ||
* @classdesc Mixin which treats the selected item in a list as the active item | ||
* in ARIA accessibility terms | ||
* | ||
* Handling ARIA selection state properly is actually quite complex. Not only | ||
* does the selected item need to be marked as selected; the other items should | ||
* be marked as *not* selected. Additionally, the outermost element with the | ||
* keyboard focus needs to have attributes set on it so that the selection is | ||
* knowable at the list level. That in turn requires that all items in the list | ||
* have ID attributes assigned to them. (To that end, this mixin will assign | ||
* generated IDs to any item that doesn't already have an ID.) | ||
*/ | ||
// Used to assign unique IDs to item elements without IDs. | ||
@@ -20,81 +5,122 @@ let idCount = 0; | ||
export default (base) => class SelectionAriaActive extends base { | ||
/* Exported function extends a base class with SelectionAriaActive. */ | ||
export default (base) => { | ||
applySelection(item, selected) { | ||
if (super.applySelection) { super.applySelection(item, selected); } | ||
item.setAttribute('aria-selected', selected); | ||
let itemId = item.getAttribute('id'); | ||
if (itemId) { | ||
this.collective.outermostElement.setAttribute('aria-activedescendant', itemId); | ||
/** | ||
* Mixin which treats the selected item in a list as the active item in ARIA | ||
* accessibility terms. | ||
* | ||
* Handling ARIA selection state properly is actually quite complex: | ||
* | ||
* * The items in the list need to be indicated as possible items via an ARIA | ||
* `role` attribute value such as "option". | ||
* * The selected item need to be marked as selected by setting the item's | ||
* `aria-selected` attribute to true *and* the other items need be marked as | ||
* *not* selected by setting `aria-selected` to false. | ||
* * The outermost element with the keyboard focus needs to have attributes | ||
* set on it so that the selection is knowable at the list level via the | ||
* `aria-activedescendant` attribute. | ||
* * Use of `aria-activedescendant` in turn requires that all items in the | ||
* list have ID attributes assigned to them. | ||
* | ||
* This mixin tries to address all of the above requirements. To that end, | ||
* this mixin will assign generated IDs to any item that doesn't already have | ||
* an ID. | ||
* | ||
* ARIA relies on elements to provide `role` attributes. This mixin will apply | ||
* a default role of "listbox" on the outer list if it doesn't already have an | ||
* explicit role. Similarly, this mixin will apply a default role of "option" | ||
* to any list item that does not already have a role specified. | ||
* | ||
* This mixin expects a set of members that manage the state of the selection: | ||
* `applySelection`, `itemAdded`, and `selectedIndex`. You can supply these | ||
* yourself, or do so via the ItemsSelection mixin. | ||
* | ||
* NOTE: For the time being, this mixin should be used with the | ||
* TargetInCollective mixin. The intention is to eventually allow this mixin | ||
* to be used without requiring collective keyboard support, so that this | ||
* mixin can be used on its own. | ||
*/ | ||
class SelectionAriaActive extends base { | ||
applySelection(item, selected) { | ||
if (super.applySelection) { super.applySelection(item, selected); } | ||
item.setAttribute('aria-selected', selected); | ||
let itemId = item.getAttribute('id'); | ||
if (itemId) { | ||
this.collective.outermostElement.setAttribute('aria-activedescendant', itemId); | ||
} | ||
} | ||
} | ||
collectiveChanged() { | ||
if (super.collectiveChanged) { super.collectiveChanged(); } | ||
collectiveChanged() { | ||
if (super.collectiveChanged) { super.collectiveChanged(); } | ||
// Ensure the outermost aspect has an ARIA role. | ||
let outermostElement = this.collective.outermostElement; | ||
if (!outermostElement.getAttribute('role')) { | ||
// Try to promote an ARIA role from an inner element. If none is found, | ||
// use a default role. | ||
let role = getCollectiveAriaRole(this.collective) || 'listbox'; | ||
outermostElement.setAttribute('role', role); | ||
} | ||
if (!outermostElement.getAttribute('aria-activedescendant')) { | ||
// Try to promote an ARIA activedescendant value from an inner element. | ||
let descendant = getCollectiveAriaActiveDescendant(this.collective); | ||
if (descendant) { | ||
outermostElement.setAttribute('aria-activedescendant', descendant); | ||
// Ensure the outermost aspect has an ARIA role. | ||
let outermostElement = this.collective.outermostElement; | ||
if (!outermostElement.getAttribute('role')) { | ||
// Try to promote an ARIA role from an inner element. If none is found, | ||
// use a default role. | ||
let role = getCollectiveAriaRole(this.collective) || 'listbox'; | ||
outermostElement.setAttribute('role', role); | ||
} | ||
if (!outermostElement.getAttribute('aria-activedescendant')) { | ||
// Try to promote an ARIA activedescendant value from an inner element. | ||
let descendant = getCollectiveAriaActiveDescendant(this.collective); | ||
if (descendant) { | ||
outermostElement.setAttribute('aria-activedescendant', descendant); | ||
} | ||
} | ||
// Remove the ARIA role and activedescendant values from the collective's | ||
// inner elements. | ||
this.collective.elements.forEach(element => { | ||
if (element !== outermostElement) { | ||
element.removeAttribute('aria-activedescendant'); | ||
element.removeAttribute('role'); | ||
} | ||
}); | ||
} | ||
// Remove the ARIA role and activedescendant values from the collective's | ||
// inner elements. | ||
this.collective.elements.forEach(element => { | ||
if (element !== outermostElement) { | ||
element.removeAttribute('aria-activedescendant'); | ||
element.removeAttribute('role'); | ||
} | ||
}); | ||
} | ||
createdCallback() { | ||
if (super.createdCallback) { super.createdCallback(); } | ||
createdCallback() { | ||
if (super.createdCallback) { super.createdCallback(); } | ||
// Determine a base item ID based on this component's host's own ID. This | ||
// will be combined with a unique integer to assign IDs to items that | ||
// don't have an explicit ID. If the basic-list-box has ID "foo", then its | ||
// items will have IDs that look like "_fooOption1". If the list has no ID | ||
// itself, its items will get IDs that look like "_option1". Item IDs are | ||
// prefixed with an underscore to differentiate them from | ||
// manually-assigned IDs, and to minimize the potential for ID conflicts. | ||
let elementId = this.getAttribute( "id" ); | ||
this.itemBaseId = elementId ? | ||
"_" + elementId + "Option" : | ||
"_option"; | ||
} | ||
// Determine a base item ID based on this component's host's own ID. This | ||
// will be combined with a unique integer to assign IDs to items that don't | ||
// have an explicit ID. If the basic-list-box has ID "foo", then its items | ||
// will have IDs that look like "_fooOption1". If the list has no ID itself, | ||
// its items will get IDs that look like "_option1". Item IDs are prefixed | ||
// with an underscore to differentiate them from manually-assigned IDs, and | ||
// to minimize the potential for ID conflicts. | ||
let elementId = this.getAttribute( "id" ); | ||
this.itemBaseId = elementId ? | ||
"_" + elementId + "Option" : | ||
"_option"; | ||
} | ||
itemAdded(item) { | ||
if (super.itemAdded) { super.itemAdded(item); } | ||
itemAdded(item) { | ||
if (super.itemAdded) { super.itemAdded(item); } | ||
item.setAttribute('role', 'option'); | ||
item.setAttribute('role', 'option'); | ||
// Ensure each item has an ID so we can set aria-activedescendant on the | ||
// overall list whenever the selection changes. | ||
if (!item.getAttribute('id')) { | ||
item.setAttribute('id', this.itemBaseId + idCount++); | ||
} | ||
} | ||
// Ensure each item has an ID so we can set aria-activedescendant on the | ||
// overall list whenever the selection changes. | ||
if (!item.getAttribute('id')) { | ||
item.setAttribute('id', this.itemBaseId + idCount++); | ||
get selectedItem() { | ||
return super.selectedItem; | ||
} | ||
} | ||
set selectedItem(item) { | ||
if ('selectedItem' in base.prototype) { super.selectedItem = item; } | ||
// Catch the case where the selection is removed. | ||
if (item == null && this.collective) { | ||
this.collective.outermostElement.removeAttribute('aria-activedescendant'); | ||
} | ||
} | ||
get selectedItem() { | ||
return super.selectedItem; | ||
} | ||
set selectedItem(item) { | ||
if ('selectedItem' in base.prototype) { super.selectedItem = item; } | ||
// Catch the case where the selection is removed. | ||
if (item == null && this.collective) { | ||
this.collective.outermostElement.removeAttribute('aria-activedescendant'); | ||
} | ||
} | ||
return SelectionAriaActive; | ||
}; | ||
@@ -101,0 +127,0 @@ |
@@ -1,15 +0,27 @@ | ||
/** | ||
* @class SelectionHighlight | ||
* @classdesc Mixin which applies standard highlight colors to a selected item | ||
*/ | ||
/* Exported function extends a base class with SelectionHighlight. */ | ||
export default (base) => { | ||
export default (base) => class SelectionHighlight extends base { | ||
/** | ||
* Mixin which applies standard highlight colors to a selected item. | ||
* | ||
* This mixin highlights textual items (e.g., in a list) in a standard way by | ||
* using the CSS `highlight` and `highlighttext` color values. These values | ||
* respect operating system defaults and user preferences, and hence are good | ||
* default values for highlight colors. | ||
* | ||
* This mixin expects an `applySelection` method to be called on an item when | ||
* its selected state changes. You can use the ItemsSelection mixin for that | ||
* purpose. | ||
*/ | ||
class SelectionHighlight extends base { | ||
applySelection(item, selected) { | ||
if (super.applySelection) { super.applySelection(item, selected); } | ||
item.style.backgroundColor = selected ? 'highlight' : ''; | ||
item.style.color = selected ? 'highlighttext' : ''; | ||
applySelection(item, selected) { | ||
if (super.applySelection) { super.applySelection(item, selected); } | ||
item.style.backgroundColor = selected ? 'highlight' : ''; | ||
item.style.color = selected ? 'highlighttext' : ''; | ||
} | ||
} | ||
return SelectionHighlight; | ||
}; |
@@ -1,65 +0,77 @@ | ||
/** | ||
* @class SelectionInView | ||
* @classdesc Mixin which scrolls a container to keep the selected item visible | ||
*/ | ||
/* Exported function extends a base class with SelectionInView. */ | ||
export default (base) => { | ||
export default (base) => class SelectionInView extends base { | ||
get selectedItem() { | ||
return super.selectedItem; | ||
} | ||
set selectedItem(item) { | ||
if ('selectedItem' in base.prototype) { super.selectedItem = item; } | ||
if (item) { | ||
// Keep the selected item in view. | ||
this.scrollItemIntoView(item); | ||
} | ||
} | ||
/** | ||
* Scroll the given element completely into view, minimizing the degree of | ||
* scrolling performed. | ||
* Mixin which scrolls a container to ensure that a newly-selected item is | ||
* visible to the user. | ||
* | ||
* Blink has a scrollIntoViewIfNeeded() function that almost the same thing, | ||
* but unfortunately it's non-standard, and in any event often ends up | ||
* scrolling more than is absolutely necessary. | ||
* When the selected item in a list-like component changes, it's easier for | ||
* the to confirm that the selection has changed to an appropriate item if the | ||
* user can actually see that item. | ||
* | ||
* @method scrollItemIntoView | ||
* This mixin expects a `selectedItem` property to be set when the selection | ||
* changes. You can supply that yourself, or use the ItemsSelection mixin. | ||
*/ | ||
scrollItemIntoView(item) { | ||
if (super.scrollItemIntoView) { super.scrollItemIntoView(); } | ||
// Get the relative position of the item with respect to the top of the | ||
// list's scrollable canvas. An item at the top of the list will have a | ||
// elementTop of 0. | ||
class SelectionInView extends base { | ||
let scrollTarget = this.scrollTarget; | ||
let elementTop = item.offsetTop - scrollTarget.offsetTop - scrollTarget.clientTop; | ||
let elementBottom = elementTop + item.offsetHeight; | ||
// Determine the bottom of the scrollable canvas. | ||
let scrollBottom = scrollTarget.scrollTop + scrollTarget.clientHeight; | ||
if (elementBottom > scrollBottom) { | ||
// Scroll up until item is entirely visible. | ||
scrollTarget.scrollTop += elementBottom - scrollBottom; | ||
get selectedItem() { | ||
return super.selectedItem; | ||
} | ||
else if (elementTop < scrollTarget.scrollTop) { | ||
// Scroll down until item is entirely visible. | ||
scrollTarget.scrollTop = elementTop; | ||
set selectedItem(item) { | ||
if ('selectedItem' in base.prototype) { super.selectedItem = item; } | ||
if (item) { | ||
// Keep the selected item in view. | ||
this.scrollItemIntoView(item); | ||
} | ||
} | ||
} | ||
/** | ||
* The element that should be scrolled with the Page Up/Down keys. | ||
* Default is the current element. | ||
* | ||
* @property scrollTarget | ||
*/ | ||
get scrollTarget() { | ||
// Prefer base result. | ||
return 'scrollTarget' in base.prototype ? super.scrollTarget : this; | ||
/** | ||
* Scroll the given element completely into view, minimizing the degree of | ||
* scrolling performed. | ||
* | ||
* Blink has a `scrollIntoViewIfNeeded()` function that does something | ||
* similar, but unfortunately it's non-standard, and in any event often ends | ||
* up scrolling more than is absolutely necessary. | ||
* | ||
* @param {HTMLElement} item - the item to scroll into view. | ||
*/ | ||
scrollItemIntoView(item) { | ||
if (super.scrollItemIntoView) { super.scrollItemIntoView(); } | ||
// Get the relative position of the item with respect to the top of the | ||
// list's scrollable canvas. An item at the top of the list will have a | ||
// elementTop of 0. | ||
let scrollTarget = this.scrollTarget; | ||
let elementTop = item.offsetTop - scrollTarget.offsetTop - scrollTarget.clientTop; | ||
let elementBottom = elementTop + item.offsetHeight; | ||
// Determine the bottom of the scrollable canvas. | ||
let scrollBottom = scrollTarget.scrollTop + scrollTarget.clientHeight; | ||
if (elementBottom > scrollBottom) { | ||
// Scroll up until item is entirely visible. | ||
scrollTarget.scrollTop += elementBottom - scrollBottom; | ||
} | ||
else if (elementTop < scrollTarget.scrollTop) { | ||
// Scroll down until item is entirely visible. | ||
scrollTarget.scrollTop = elementTop; | ||
} | ||
} | ||
/** | ||
* The element that should be scrolled to bring an item into view. | ||
* | ||
* The default value of this property is the element itself. | ||
* | ||
* @type {HTMLElement} | ||
*/ | ||
get scrollTarget() { | ||
// Prefer base result. | ||
return 'scrollTarget' in base.prototype ? super.scrollTarget : this; | ||
} | ||
set scrollTarget(element) { | ||
if ('scrollTarget' in base.prototype) { super.scrollTarget = element; } | ||
} | ||
} | ||
set scrollTarget(element) { | ||
if ('scrollTarget' in base.prototype) { super.scrollTarget = element; } | ||
} | ||
return SelectionInView; | ||
}; |
@@ -1,35 +0,47 @@ | ||
/** | ||
* @class ShadowElementReferences | ||
* @classdesc Mixin to create references to elements in a component's Shadow | ||
* DOM subtree | ||
* | ||
* This adds a member on the component called `$` that can be used to reference | ||
* shadow elements with IDs. E.g., if component's shadow contains an element | ||
* `<button id="foo">`, then this mixin will create a member `this.$.foo` that | ||
* points to that button. Such references simplify a component's access to its | ||
* own elements. | ||
* | ||
* This trades off a one-time cost of querying all elements in the shadow tree | ||
* against having to query for an element each time the component wants to | ||
* inspect or manipulate it. | ||
* | ||
* This mixin is inspired by Polymer's automatic node finding feature. | ||
* See https://www.polymer-project.org/1.0/docs/devguide/local-dom.html#node-finding. | ||
*/ | ||
/* Exported function extends a base class with ShadowElementReferences. */ | ||
export default (base) => { | ||
/** | ||
* Mixin to create references to elements in a component's Shadow DOM subtree. | ||
* | ||
* This adds a member on the component called `this.$` that can be used to | ||
* reference shadow elements with IDs. E.g., if component's shadow contains an | ||
* element `<button id="foo">`, then this mixin will create a member | ||
* `this.$.foo` that points to that button. | ||
* | ||
* Such references simplify a component's access to its own elements. In | ||
* exchange, this mixin trades off a one-time cost of querying all elements in | ||
* the shadow tree instead of paying an ongoing cost to query for an element | ||
* each time the component wants to inspect or manipulate it. | ||
* | ||
* This mixin expects the component to define a Shadow DOM subtree. You can | ||
* create that tree yourself, or make use of the ShadowTemplate mixin. | ||
* | ||
* This mixin is inspired by Polymer's [automatic | ||
* node finding](https://www.polymer-project.org/1.0/docs/devguide/local-dom.html#node-finding) | ||
* feature. | ||
*/ | ||
class ShadowElementReferences extends base { | ||
export default (base) => class ShadowElementReferences extends base { | ||
createdCallback() { | ||
if (super.createdCallback) { super.createdCallback(); } | ||
if (this.shadowRoot) { | ||
// Look for elements in the shadow subtree that have id attributes. | ||
// An alternatively implementation of this mixin would be to just define | ||
// a this.$ getter that lazily does this search the first time someone | ||
// tries to access this.$. That might introduce some complexity – if the | ||
// the tree changed after it was first populated, the result of | ||
// searching for a node might be somewhat unpredictable. | ||
this.$ = {}; | ||
let nodesWithIds = this.shadowRoot.querySelectorAll('[id]'); | ||
[].forEach.call(nodesWithIds, node => { | ||
let id = node.getAttribute('id'); | ||
this.$[id] = node; | ||
}); | ||
} | ||
} | ||
createdCallback() { | ||
if (super.createdCallback) { super.createdCallback(); } | ||
if (this.shadowRoot) { | ||
this.$ = {}; | ||
let nodesWithIds = this.shadowRoot.querySelectorAll('[id]'); | ||
[].forEach.call(nodesWithIds, node => { | ||
let id = node.getAttribute('id'); | ||
this.$[id] = node; | ||
}); | ||
} | ||
} | ||
return ShadowElementReferences; | ||
}; |
@@ -1,16 +0,1 @@ | ||
/** | ||
* @class ShadowTemplate | ||
* @classdesc Mixin for stamping a template into a Shadow DOM subtree upon | ||
* component instantiation | ||
* | ||
* If a component defines a template property (as a string or referencing a HTML | ||
* template), when the component class is instantiated, a shadow root will be | ||
* created on the instance, and the contents of the template will be cloned into | ||
* the shadow root. | ||
* | ||
* For the time being, this extension retains support for Shadow DOM v0. | ||
* That will eventually be deprecated as browsers implement Shadow DOM v1. | ||
*/ | ||
// Feature detection for old Shadow DOM v0. | ||
@@ -20,37 +5,65 @@ const USING_SHADOW_DOM_V0 = (typeof HTMLElement.prototype.createShadowRoot !== 'undefined'); | ||
export default (base) => class ShadowTemplate extends base { | ||
/* Exported function extends a base class with ShadowTemplate. */ | ||
export default (base) => { | ||
/* | ||
* If the component defines a template, a shadow root will be created on the | ||
* component instance, and the template stamped into it. | ||
/** | ||
* Mixin for stamping a template into a Shadow DOM subtree upon component | ||
* instantiation. | ||
* | ||
* To use this mixin, define a `template` property as a string or HTML | ||
* `<template>` element: | ||
* | ||
* class MyElement extends ShadowTemplate(HTMLElement) { | ||
* get template() { | ||
* return `Hello, <em>world</em>.`; | ||
* } | ||
* } | ||
* | ||
* When your component class is instantiated, a shadow root will be created on | ||
* the instance, and the contents of the template will be cloned into the | ||
* shadow root. If your component does not define a `template` property, this | ||
* mixin has no effect. | ||
* | ||
* For the time being, this extension retains support for Shadow DOM v0. That | ||
* will eventually be deprecated as browsers (and the Shadow DOM polyfill) | ||
* implement Shadow DOM v1. | ||
*/ | ||
createdCallback() { | ||
if (super.createdCallback) { super.createdCallback(); } | ||
let template = this.template; | ||
// TODO: Save the processed template with the component's class prototype | ||
// so it doesn't need to be processed with every instantiation. | ||
if (template) { | ||
class ShadowTemplate extends base { | ||
if (typeof template === 'string') { | ||
// Upgrade plain string to real template. | ||
template = createTemplateWithInnerHTML(template); | ||
} | ||
/* | ||
* If the component defines a template, a shadow root will be created on the | ||
* component instance, and the template stamped into it. | ||
*/ | ||
createdCallback() { | ||
if (super.createdCallback) { super.createdCallback(); } | ||
let template = this.template; | ||
// TODO: Save the processed template with the component's class prototype | ||
// so it doesn't need to be processed with every instantiation. | ||
if (template) { | ||
if (USING_SHADOW_DOM_V0) { | ||
polyfillSlotWithContent(template); | ||
} | ||
if (typeof template === 'string') { | ||
// Upgrade plain string to real template. | ||
template = createTemplateWithInnerHTML(template); | ||
} | ||
if (window.ShadowDOMPolyfill) { | ||
shimTemplateStyles(template, this.localName); | ||
if (USING_SHADOW_DOM_V0) { | ||
polyfillSlotWithContent(template); | ||
} | ||
if (window.ShadowDOMPolyfill) { | ||
shimTemplateStyles(template, this.localName); | ||
} | ||
// this.log("cloning template into shadow root"); | ||
let root = USING_SHADOW_DOM_V0 ? | ||
this.createShadowRoot() : // Shadow DOM v0 | ||
this.attachShadow({ mode: 'open' }); // Shadow DOM v1 | ||
let clone = document.importNode(template.content, true); | ||
root.appendChild(clone); | ||
} | ||
} | ||
// this.log("cloning template into shadow root"); | ||
let root = USING_SHADOW_DOM_V0 ? | ||
this.createShadowRoot() : // Shadow DOM v0 | ||
this.attachShadow({ mode: 'open' }); // Shadow DOM v1 | ||
let clone = document.importNode(template.content, true); | ||
root.appendChild(clone); | ||
} | ||
} | ||
return ShadowTemplate; | ||
}; | ||
@@ -57,0 +70,0 @@ |
@@ -1,78 +0,106 @@ | ||
/** | ||
* @class SwipeDirection | ||
* @classdesc Mixin which maps touch gestures (swipe left, swipe right) to direction | ||
* semantics (goRight, goLeft) | ||
*/ | ||
/* Exported function extends a base class with SwipeDirection. */ | ||
export default (base) => { | ||
/** | ||
* Mixin which maps touch gestures (swipe left, swipe right) to direction | ||
* semantics (go right, go left). | ||
* | ||
* By default, this mixin presents no user-visible effects; it just indicates a | ||
* direction in which the user is currently swiping or has finished swiping. To | ||
* map the direction to a change in selection, use the DirectionSelection mixin. | ||
*/ | ||
class SwipeDirection extends base { | ||
export default (base) => class SwipeDirection extends base { | ||
createdCallback() { | ||
if (super.createdCallback) { super.createdCallback(); } | ||
createdCallback() { | ||
if (super.createdCallback) { super.createdCallback(); } | ||
this.position = 0; | ||
this.position = 0; | ||
// TODO: Touch events could be factored out into its own mixin. | ||
// TODO: Touch events could be factored out into its own mixin. | ||
// In all touch events, only handle single touches. We don't want to | ||
// inadvertently do work when the user's trying to pinch-zoom for example. | ||
// TODO: Even better approach than below would be to ignore touches after | ||
// the first if the user has already begun a swipe. | ||
this.addEventListener('touchstart', event => { | ||
if (this._multiTouch) { | ||
return; | ||
} else if (event.touches.length === 1) { | ||
touchStart(this, event); | ||
} else { | ||
this._multiTouch = true; | ||
} | ||
}); | ||
this.addEventListener('touchmove', event => { | ||
if (!this._multiTouch && event.touches.length === 1) { | ||
let handled = touchMove(this, event); | ||
if (handled) { | ||
event.preventDefault(); | ||
// In all touch events, only handle single touches. We don't want to | ||
// inadvertently do work when the user's trying to pinch-zoom for example. | ||
// TODO: Even better approach than below would be to ignore touches after | ||
// the first if the user has already begun a swipe. | ||
this.addEventListener('touchstart', event => { | ||
if (this._multiTouch) { | ||
return; | ||
} else if (event.touches.length === 1) { | ||
touchStart(this, event); | ||
} else { | ||
this._multiTouch = true; | ||
} | ||
} | ||
}); | ||
this.addEventListener('touchend', event => { | ||
if (event.touches.length === 0) { | ||
// All touches removed; gesture is complete. | ||
if (!this._multiTouch) { | ||
// Single-touch swipe has finished. | ||
touchEnd(this, event); | ||
}); | ||
this.addEventListener('touchmove', event => { | ||
if (!this._multiTouch && event.touches.length === 1) { | ||
let handled = touchMove(this, event); | ||
if (handled) { | ||
event.preventDefault(); | ||
} | ||
} | ||
this._multiTouch = false; | ||
} | ||
}); | ||
} | ||
}); | ||
this.addEventListener('touchend', event => { | ||
if (event.touches.length === 0) { | ||
// All touches removed; gesture is complete. | ||
if (!this._multiTouch) { | ||
// Single-touch swipe has finished. | ||
touchEnd(this, event); | ||
} | ||
this._multiTouch = false; | ||
} | ||
}); | ||
} | ||
// Default implementations | ||
goLeft() { | ||
if (super.goLeft) { return super.goLeft(); } | ||
} | ||
goRight() { | ||
if (super.goRight) { return super.goRight(); } | ||
} | ||
/** | ||
* Invoked when the user wants to go/navigate left. | ||
* The default implementation of this method does nothing. | ||
*/ | ||
goLeft() { | ||
if (super.goLeft) { return super.goLeft(); } | ||
} | ||
/** | ||
* The distance the user has moved the first touchpoint since the beginning | ||
* of a drag, expressed as a fraction of the element's width. | ||
* | ||
* @property position | ||
* @type Number | ||
*/ | ||
get position() { | ||
return this._position; | ||
} | ||
set position(position) { | ||
if ('position' in base.prototype) { super.position = position; } | ||
this._position = position; | ||
} | ||
/** | ||
* Invoked when the user wants to go/navigate right. | ||
* The default implementation of this method does nothing. | ||
*/ | ||
goRight() { | ||
if (super.goRight) { return super.goRight(); } | ||
} | ||
// Default implementation | ||
showTransition(value) { | ||
if (super.showTransition) { super.showTransition(value); } | ||
/** | ||
* The distance the user has moved the first touchpoint since the beginning | ||
* of a drag, expressed as a fraction of the element's width. | ||
* | ||
* @type number | ||
*/ | ||
get position() { | ||
return this._position; | ||
} | ||
set position(position) { | ||
if ('position' in base.prototype) { super.position = position; } | ||
this._position = position; | ||
} | ||
/** | ||
* Determine whether a transition should be shown during a swipe. | ||
* | ||
* Components like carousels often define animated CSS transitions for | ||
* sliding effects. Such a transition should usually *not* be applied while | ||
* the user is dragging, because a CSS animation will introduce a lag that | ||
* makes the swipe feel sluggish. Instead, as long as the user is dragging | ||
* with their finger down, the transition should be suppressed. When the | ||
* user releases their finger, the transition can be restored, allowing the | ||
* animation to show the carousel sliding into its final position. | ||
* | ||
* @param {boolean} value - true if a component-provided transition should | ||
* be shown, false if not. | ||
*/ | ||
// TODO: Rename (and flip meaning) to something like dragging()? | ||
showTransition(value) { | ||
if (super.showTransition) { super.showTransition(value); } | ||
} | ||
} | ||
return SwipeDirection; | ||
}; | ||
@@ -79,0 +107,0 @@ |
@@ -1,25 +0,49 @@ | ||
/** | ||
* @class TargetInCollective | ||
* @classdesc Mixin which allows a component to provide aggregate behavior with | ||
* other elements, e.g., for keyboard handling | ||
*/ | ||
import Collective from './Collective'; | ||
import Collective from './Collective'; | ||
/* Exported function extends a base class with TargetInCollective. */ | ||
export default (base) => { | ||
export default (base) => class TargetInCollective extends base { | ||
/** | ||
* Mixin which allows a component to provide aggregate behavior with other | ||
* elements, e.g., for keyboard handling. | ||
* | ||
* This mixin implicitly creates a collective for a component so that it can | ||
* participate in collective keyboard handling. See the Collective class for | ||
* details. | ||
* | ||
* You can use this mixin in conjunction with ContentFirstChildTarget to | ||
* automatically have the component's collective extended to its first child. | ||
*/ | ||
class TargetInCollective extends base { | ||
createdCallback() { | ||
if (super.createdCallback) { super.createdCallback(); } | ||
this.collective = new Collective(this); | ||
} | ||
createdCallback() { | ||
if (super.createdCallback) { super.createdCallback(); } | ||
this.collective = new Collective(this); | ||
} | ||
get target() { | ||
return super.target; | ||
/** | ||
* Gets/sets the current target of the component. | ||
* | ||
* Set this to point to another element. That target element will be | ||
* implicitly added to the component's collective. That is, the component | ||
* and its target will share responsibility for handling keyboard events. | ||
* | ||
* You can set this property yourself, or you can use the | ||
* ContentFirstChildTarget mixin to automatically set the target to the | ||
* component's first child. | ||
* | ||
* @type {HTMLElement} | ||
*/ | ||
get target() { | ||
return super.target; | ||
} | ||
set target(element) { | ||
if ('target' in base.prototype) { super.target = element; } | ||
this.collective.assimilate(element); | ||
} | ||
} | ||
set target(element) { | ||
if ('target' in base.prototype) { super.target = element; } | ||
this.collective.assimilate(element); | ||
} | ||
return TargetInCollective; | ||
}; |
@@ -1,111 +0,152 @@ | ||
/** | ||
* @class TargetSelection | ||
* @classdesc Mixin that allows a component to delegate its own selection | ||
* semantics to a target element | ||
* | ||
* This is useful when defining components that act as optional decorators for a | ||
* component that acts like a list. | ||
*/ | ||
/* Exported function extends a base class with TargetSelection. */ | ||
export default (base) => { | ||
/** | ||
* Mixin which allows a component to delegate its own selection semantics to a | ||
* target element. | ||
* | ||
* This is useful when defining components that act as optional features for a | ||
* component that acts like a list. See basic-arrow-selection and | ||
* basic-page-dots for examples of components used as optional features for | ||
* components like basic-carousel. A typical usage might be: | ||
* | ||
* <basic-arrow-selection> | ||
* <basic-carousel> | ||
* ... images, etc. ... | ||
* </basic-carousel> | ||
* </basic-arrow-selection> | ||
* | ||
* Because basic-arrow-selection uses the TargetSelection mixin, it exposes | ||
* members to access a selection: `selectNext`, `selectPrevious`, | ||
* `selectedIndex`, etc. These are all delegated to the child component (here, | ||
* a basic-carousel). | ||
* | ||
* This mixin expects a `target` property to be set to the element actually | ||
* managing the selection. You can set that property yourself, or you can use | ||
* the ContentFirstChildTarget mixin to implicitly take the component's first | ||
* child as the target. This is what basic-arrow-selection (above) does. | ||
*/ | ||
class TargetSelection extends base { | ||
export default (base) => class TargetSelection extends base { | ||
// attachedCallback() { | ||
// // Apply any selection made before assimilation. | ||
// if (this._prematureSelectedIndex | ||
// && 'selectedIndex' in this && this.selectedIndex === -1) { | ||
// this.selectedIndex = this._prematureSelectedIndex; | ||
// this._prematureSelectedIndex = null; | ||
// } | ||
// } | ||
// attachedCallback() { | ||
// // Apply any selection made before assimilation. | ||
// if (this._prematureSelectedIndex | ||
// && 'selectedIndex' in this && this.selectedIndex === -1) { | ||
// this.selectedIndex = this._prematureSelectedIndex; | ||
// this._prematureSelectedIndex = null; | ||
// } | ||
// } | ||
/** | ||
* Return the positional index for the indicated item. | ||
* | ||
* @param {HTMLElement} item The item whose index is requested. | ||
* @returns {number} The index of the item, or -1 if not found. | ||
*/ | ||
indexOfItem(item) { | ||
if (super.indexOfItem) { super.indexOfItem(item); } | ||
let target = this.target; | ||
return target ? | ||
target.indexOfItem(item) : | ||
-1; | ||
} | ||
indexOfItem(item) { | ||
if (super.indexOfItem) { super.indexOfItem(item); } | ||
let target = this.target; | ||
return target ? | ||
target.indexOfItem(item) : | ||
-1; | ||
} | ||
/** | ||
* The current set of items in the list. | ||
* | ||
* @type {HTMLElement[]} | ||
*/ | ||
get items() { | ||
let target = this.target; | ||
let items = target && target.items; | ||
return items || []; | ||
} | ||
get items() { | ||
let target = this.target; | ||
let items = target && target.items; | ||
return items || []; | ||
} | ||
/** | ||
* This method is invoked when the underlying contents change. It is also | ||
* invoked on component initialization – since the items have "changed" from | ||
* being nothing. | ||
*/ | ||
itemsChanged() { | ||
if (super.itemsChanged) { super.itemsChanged(); } | ||
this.dispatchEvent(new CustomEvent('items-changed')); | ||
} | ||
itemsChanged() { | ||
if (super.itemsChanged) { super.itemsChanged(); } | ||
this.dispatchEvent(new CustomEvent('items-changed')); | ||
} | ||
/** | ||
* The index of the item which is currently selected, or -1 if there is no | ||
* selection. | ||
* | ||
* @type {number} | ||
*/ | ||
get selectedIndex() { | ||
let target = this.target; | ||
return target && target.selectedIndex; | ||
} | ||
set selectedIndex(index) { | ||
if ('selectedIndex' in base.prototype) { super.selectedIndex = index; } | ||
// if ('selectedIndex' in this { | ||
// this.selectedIndex = index; | ||
// } else { | ||
// // Selection is being made before the collective supports it. | ||
// this._prematureSelectedIndex = index; | ||
// } | ||
let target = this.target; | ||
if (target && target.selectedIndex !== index) { | ||
target.selectedIndex = index; | ||
} | ||
} | ||
/** | ||
* The index of the item which is currently selected, or -1 if there is no | ||
* selection. | ||
* | ||
* @property selectedIndex | ||
* @type Number | ||
*/ | ||
get selectedIndex() { | ||
let target = this.target; | ||
return target && target.selectedIndex; | ||
} | ||
set selectedIndex(index) { | ||
if ('selectedIndex' in base.prototype) { super.selectedIndex = index; } | ||
// if ('selectedIndex' in this { | ||
// this.selectedIndex = index; | ||
// } else { | ||
// // Selection is being made before the collective supports it. | ||
// this._prematureSelectedIndex = index; | ||
// } | ||
let target = this.target; | ||
if (target && target.selectedIndex !== index) { | ||
target.selectedIndex = index; | ||
/** | ||
* The currently selected item, or null if there is no selection. | ||
* | ||
* @type {HTMLElement} | ||
*/ | ||
get selectedItem() { | ||
let target = this.target; | ||
return target && target.selectedItem; | ||
} | ||
} | ||
set selectedItem(item) { | ||
if ('selectedItem' in base.prototype) { super.selectedItem = item; } | ||
let target = this.target; | ||
if (target) { | ||
target.selectedItem = item; | ||
} | ||
} | ||
/** | ||
* The currently selected item, or null if there is no selection. | ||
* | ||
* @property selectedItem | ||
* @type Object | ||
*/ | ||
get selectedItem() { | ||
let target = this.target; | ||
return target && target.selectedItem; | ||
} | ||
set selectedItem(item) { | ||
if ('selectedItem' in base.prototype) { super.selectedItem = item; } | ||
let target = this.target; | ||
if (target) { | ||
target.selectedItem = item; | ||
selectedItemChanged() { | ||
if (super.selectedItemChanged) { super.selectedItemChanged(); } | ||
} | ||
} | ||
selectedItemChanged() { | ||
if (super.selectedItemChanged) { super.selectedItemChanged(); } | ||
} | ||
get target() { | ||
return super.target; | ||
} | ||
set target(element) { | ||
if ('target' in base.prototype) { super.target = element; } | ||
if (this._itemsChangedListener) { | ||
this.removeEventListener('items-changed', this._itemsChangedListener); | ||
/** | ||
* Gets/sets the target element to which this component will delegate | ||
* selection actions. | ||
* | ||
* @type {HTMLElement} | ||
*/ | ||
get target() { | ||
return super.target; | ||
} | ||
if (this._selectedItemChangedListener) { | ||
this.removeEventListener('selected-item-changed', this._selectedItemChangedListener); | ||
set target(element) { | ||
if ('target' in base.prototype) { super.target = element; } | ||
if (this._itemsChangedListener) { | ||
this.removeEventListener('items-changed', this._itemsChangedListener); | ||
} | ||
if (this._selectedItemChangedListener) { | ||
this.removeEventListener('selected-item-changed', this._selectedItemChangedListener); | ||
} | ||
this._itemsChangedListener = element.addEventListener('items-changed', event => { | ||
this.itemsChanged(); | ||
}); | ||
this._selectedItemChangedListener = element.addEventListener('selected-item-changed', event => { | ||
// Let the component know the target's selection changed, but without | ||
// re-invoking the selectIndex/selectedItem setter. | ||
this.selectedItemChanged(); | ||
}); | ||
// Force initial refresh. | ||
this.itemsChanged(); | ||
} | ||
this._itemsChangedListener = element.addEventListener('items-changed', event => { | ||
this.itemsChanged(); | ||
}); | ||
this._selectedItemChangedListener = element.addEventListener('selected-item-changed', event => { | ||
// Let the component know the target's selection changed, but without | ||
// re-invoking the selectIndex/selectedItem setter. | ||
this.selectedItemChanged(); | ||
}); | ||
// Force initial refresh. | ||
this.itemsChanged(); | ||
} | ||
return TargetSelection; | ||
}; |
@@ -1,72 +0,80 @@ | ||
/** | ||
* @class TimerSelection | ||
* @classdesc Mixin provides for automatic timed changes in selection, as in a | ||
* automated slideshow | ||
*/ | ||
/* Exported function extends a base class with TimerSelection. */ | ||
export default (base) => { | ||
export default (base) => class TimerSelection extends base { | ||
contentChanged() { | ||
if (super.contentChanged) { super.contentChanged(); } | ||
this.play(); | ||
} | ||
/** | ||
* Begin automatic progression of the selection. | ||
* Mixin which provides for automatic timed changes in selection. | ||
* | ||
* @method play | ||
*/ | ||
play() { | ||
if (super.play) { super.play(); } | ||
this._playing = true; | ||
setTimer(this); | ||
} | ||
/** | ||
* Pause automatic progression of the selection. | ||
* This mixin is useful for creating slideshow-like elements. | ||
* | ||
* @method pause | ||
* This mixin expects the component to define an `items` property, as well as | ||
* `selectFirst` and `selectNext` methods. You can implement those yourself, | ||
* or use the ContentAsItems and ItemsSelection mixins. | ||
*/ | ||
pause() { | ||
if (super.pause) { super.pause(); } | ||
clearTimer(this); | ||
this._playing = false; | ||
} | ||
class TimerSelection extends base { | ||
/** | ||
* True if the selection is being automatically advanced. | ||
* | ||
* @property playing | ||
* @type Boolean | ||
*/ | ||
get playing() { | ||
return this._playing; | ||
} | ||
set playing(playing) { | ||
if ('playing' in base.prototype) { super.playing = playing; } | ||
if (playing && !this._playing) { | ||
contentChanged() { | ||
if (super.contentChanged) { super.contentChanged(); } | ||
this.play(); | ||
} else if (!playing && this._playing) { | ||
this.pause(); | ||
} | ||
} | ||
// Whether the user has selected an item manually, or we've automatically | ||
// advanced the selection, we wait for a bit before advancing again. | ||
get selectedItem() { | ||
return super.selectedItem; | ||
} | ||
set selectedItem(item) { | ||
if ('selectedItem' in base.prototype) { super.selectedItem = item; } | ||
clearTimer(this); | ||
if (this.playing) { | ||
/** | ||
* Begin automatic progression of the selection. | ||
*/ | ||
play() { | ||
if (super.play) { super.play(); } | ||
this._playing = true; | ||
setTimer(this); | ||
} | ||
/** | ||
* Pause automatic progression of the selection. | ||
*/ | ||
pause() { | ||
if (super.pause) { super.pause(); } | ||
clearTimer(this); | ||
this._playing = false; | ||
} | ||
/** | ||
* True if the selection is being automatically advanced. | ||
* | ||
* @type {boolean} | ||
*/ | ||
get playing() { | ||
return this._playing; | ||
} | ||
set playing(playing) { | ||
if ('playing' in base.prototype) { super.playing = playing; } | ||
if (playing && !this._playing) { | ||
this.play(); | ||
} else if (!playing && this._playing) { | ||
this.pause(); | ||
} | ||
} | ||
/* | ||
* When the selected item changes (because of something this mixin did, | ||
* or was changed by an outside agent like the user), we wait a bit before | ||
* advancing to the next item. By triggering the next item this way, | ||
* we implicitly get a desirable behavior: if the user changes the selection | ||
* (e.g., in a carousel), we let them see that selection state for a while | ||
* before advancing the selection ourselves. | ||
*/ | ||
get selectedItem() { | ||
return super.selectedItem; | ||
} | ||
set selectedItem(item) { | ||
if ('selectedItem' in base.prototype) { super.selectedItem = item; } | ||
clearTimer(this); | ||
if (this.playing) { | ||
setTimer(this); | ||
} | ||
} | ||
} | ||
return TimerSelection; | ||
}; | ||
function clearTimer(element) { | ||
@@ -73,0 +81,0 @@ if (element._timeout) { |
@@ -1,57 +0,78 @@ | ||
/** | ||
* @class TrackpadDirection | ||
* @classdesc Mixin which maps a horizontal trackpad swipe gestures (or | ||
* horizontal mouse wheel actions) to direction semantics | ||
* | ||
* To respond to the trackpad, we can listen to the DOM's "wheel" events. These | ||
* events are fired as the user drags their fingers across a trackpad. | ||
* Unfortunately, this scheme is missing a critical event — there is no event | ||
* when the user *stops* a gestured on the trackpad. | ||
* | ||
* To complicate matters, the mainstream browsers continue to generate wheel | ||
* events even after the user has stopped dragging their fingers. These fake | ||
* events simulate the user gradually slowing down the drag until they come to a | ||
* smooth stop. In some contexts, these fake wheel events might be helpful, but | ||
* in trying to supply typical trackpad swipe navigation, these fake events get | ||
* in the way. | ||
* | ||
* This component uses some heuristics to work around these problems, but the | ||
* complex nature of the problem make it extremely difficult to achieve the same | ||
* degree of trackpad responsiveness possible with native applications. | ||
*/ | ||
/* Exported function extends a base class with TrackpadDirection. */ | ||
export default (base) => { | ||
/** | ||
* Mixin which maps a horizontal trackpad swipe gestures (or horizontal mouse | ||
* wheel actions) to direction semantics. | ||
* | ||
* You can use this mixin with a mixin like DirectionSelection to let the user | ||
* change the selection with the trackpad or mouse wheel. | ||
* | ||
* To respond to the trackpad, we can listen to the DOM's "wheel" events. | ||
* These events are fired as the user drags their fingers across a trackpad. | ||
* Unfortunately, browsers are missing a critical event — there is no event | ||
* when the user *stops* a gestured on the trackpad or mouse wheel. | ||
* | ||
* To make things worse, the mainstream browsers continue to generate fake | ||
* wheel events even after the user has stopped dragging their fingers. These | ||
* fake events simulate the user gradually slowing down the drag until they | ||
* come to a smooth stop. In some contexts, these fake wheel events might be | ||
* helpful, but in trying to supply typical trackpad swipe navigation, these | ||
* fake events get in the way. | ||
* | ||
* This component uses heuristics to work around these problems, but the | ||
* complex nature of the problem make it extremely difficult to achieve the | ||
* same degree of trackpad responsiveness possible with native applications. | ||
*/ | ||
class TrackpadDirection extends base { | ||
export default (base) => class TrackpadDirection extends base { | ||
createdCallback() { | ||
if (super.createdCallback) { super.createdCallback(); } | ||
this.addEventListener('wheel', event => { | ||
let handled = wheel(this, event); | ||
if (handled) { | ||
event.preventDefault(); | ||
} | ||
}); | ||
resetWheelTracking(this); | ||
} | ||
createdCallback() { | ||
if (super.createdCallback) { super.createdCallback(); } | ||
this.addEventListener('wheel', event => { | ||
let handled = wheel(this, event); | ||
if (handled) { | ||
event.preventDefault(); | ||
} | ||
}); | ||
resetWheelTracking(this); | ||
} | ||
/** | ||
* Invoked when the user wants to go/navigate left. | ||
* The default implementation of this method does nothing. | ||
*/ | ||
goLeft() { | ||
if (super.goLeft) { return super.goLeft(); } | ||
} | ||
// Default implementations | ||
goLeft() { | ||
if (super.goLeft) { return super.goLeft(); } | ||
} | ||
goRight() { | ||
if (super.goRight) { return super.goRight(); } | ||
} | ||
/** | ||
* Invoked when the user wants to go/navigate right. | ||
* The default implementation of this method does nothing. | ||
*/ | ||
goRight() { | ||
if (super.goRight) { return super.goRight(); } | ||
} | ||
get position() { | ||
return super.position; | ||
} | ||
set position(position) { | ||
if ('position' in base.prototype) { super.position = position; } | ||
} | ||
/** | ||
* The distance the user has moved the first touchpoint since the beginning | ||
* of a trackpad/wheel operation, expressed as a fraction of the element's | ||
* width. | ||
* | ||
* @type number | ||
*/ | ||
get position() { | ||
return super.position; | ||
} | ||
set position(position) { | ||
if ('position' in base.prototype) { super.position = position; } | ||
} | ||
// Default implementation | ||
showTransition(value) { | ||
if (super.showTransition) { super.showTransition(value); } | ||
// Default implementation | ||
showTransition(value) { | ||
if (super.showTransition) { super.showTransition(value); } | ||
} | ||
} | ||
return TrackpadDirection; | ||
}; | ||
@@ -58,0 +79,0 @@ |
Sorry, the diff of this file is too big to display
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
603335
7936
347
0