diff --git a/packages/all/mdc-all.scss b/packages/all/mdc-all.scss index 86c82051..517b9d45 100644 --- a/packages/all/mdc-all.scss +++ b/packages/all/mdc-all.scss @@ -1,5 +1,6 @@ @use "@material/button/mdc-button"; @use "@material/list/evolution-mixins"as list-evolution-mixins; +@use "@material/list/mdc-list"; @use "@aurelia-mdc-web/list"as auList; @use "@material/top-app-bar/mdc-top-app-bar"; @use "@material/icon-button/mdc-icon-button"; diff --git a/packages/app/src/views/drawer/dismissible.html b/packages/app/src/views/drawer/dismissible.html index 5ea49aca..af1d4cc1 100644 --- a/packages/app/src/views/drawer/dismissible.html +++ b/packages/app/src/views/drawer/dismissible.html @@ -3,12 +3,12 @@ - - - ${item.icon} + + + ${item.icon} ${item.label} - - + + diff --git a/packages/app/src/views/drawer/modal.html b/packages/app/src/views/drawer/modal.html index 721713d8..643f3cef 100644 --- a/packages/app/src/views/drawer/modal.html +++ b/packages/app/src/views/drawer/modal.html @@ -3,12 +3,12 @@ - - - ${item.icon} + + + ${item.icon} ${item.label} - - + +
diff --git a/packages/app/src/views/drawer/rtl.html b/packages/app/src/views/drawer/rtl.html index c1b4ea9b..e3e1c9e3 100644 --- a/packages/app/src/views/drawer/rtl.html +++ b/packages/app/src/views/drawer/rtl.html @@ -3,12 +3,12 @@ - - - ${item.icon} + + + ${item.icon} ${item.label} - - + +
diff --git a/packages/app/src/views/drawer/standard.html b/packages/app/src/views/drawer/standard.html index f644c180..db02ca04 100644 --- a/packages/app/src/views/drawer/standard.html +++ b/packages/app/src/views/drawer/standard.html @@ -3,38 +3,38 @@ - - + - ${item.icon} + ${item.icon} ${item.label} - - + +
Labels
-
- - bookmark + + + bookmark Family - - bookmark + + bookmark Friends - - bookmark + + bookmark Work - - - settings + + + settings Settings - - announcement + + announcement Help & feedback -
+
diff --git a/packages/app/src/views/root/root.html b/packages/app/src/views/root/root.html index 38785294..c0487127 100644 --- a/packages/app/src/views/root/root.html +++ b/packages/app/src/views/root/root.html @@ -23,12 +23,12 @@ - + - + diff --git a/packages/drawer/src/mdc-drawer.ts b/packages/drawer/src/mdc-drawer.ts index 16b5ed63..d5755929 100644 --- a/packages/drawer/src/mdc-drawer.ts +++ b/packages/drawer/src/mdc-drawer.ts @@ -1,7 +1,6 @@ import { MdcComponent, MdcFocusTrap } from '@aurelia-mdc-web/base'; import { MDCDismissibleDrawerFoundation, cssClasses, strings, MDCModalDrawerFoundation, MDCDrawerAdapter } from '@material/drawer'; import { SpecificEventListener } from '@material/base'; -import { MDCListFoundation } from '@material/list'; import { inject, useView, customElement, bindable } from 'aurelia-framework'; import { PLATFORM } from 'aurelia-pal'; @@ -106,8 +105,7 @@ export class MdcDrawer extends MdcComponent { - const activeNavItemEl = this.root.querySelector( - `.${MDCListFoundation.cssClasses.LIST_ITEM_ACTIVATED_CLASS}`); + const activeNavItemEl = this.root.querySelector('.mdc-deprecated-list-item--activated'); if (activeNavItemEl) { activeNavItemEl.focus(); } diff --git a/packages/list/src/index.ts b/packages/list/src/index.ts index ba649e31..475ae80a 100644 --- a/packages/list/src/index.ts +++ b/packages/list/src/index.ts @@ -15,6 +15,17 @@ export function configure(config: FrameworkConfiguration) { PLATFORM.moduleName('./mdc-list-item-secondary-text'), PLATFORM.moduleName('./mdc-list-divider/mdc-list-divider'), PLATFORM.moduleName('./mdc-list-group'), + + PLATFORM.moduleName('./mdc-deprecated-list/mdc-deprecated-list'), + PLATFORM.moduleName('./mdc-deprecated-list/mdc-deprecated-list-item/mdc-deprecated-list-item'), + PLATFORM.moduleName('./mdc-deprecated-list/mdc-deprecated-list-item/enhance-mdc-deprecated-list-item'), + PLATFORM.moduleName('./mdc-deprecated-list/mdc-deprecated-list-item-primary-text'), + PLATFORM.moduleName('./mdc-deprecated-list/mdc-deprecated-list-item-secondary-text'), + PLATFORM.moduleName('./mdc-deprecated-list/mdc-deprecated-list-item-graphic'), + PLATFORM.moduleName('./mdc-deprecated-list/mdc-deprecated-list-item-meta'), + PLATFORM.moduleName('./mdc-deprecated-list/mdc-deprecated-list-divider/mdc-deprecated-list-divider'), + PLATFORM.moduleName('./mdc-deprecated-list/mdc-deprecated-list-group'), + ]); config.aurelia.use.plugin(PLATFORM.moduleName('@aurelia-mdc-web/ripple')); diff --git a/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-divider/mdc-deprecated-list-divider.html b/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-divider/mdc-deprecated-list-divider.html new file mode 100644 index 00000000..414f54b2 --- /dev/null +++ b/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-divider/mdc-deprecated-list-divider.html @@ -0,0 +1,3 @@ + diff --git a/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-divider/mdc-deprecated-list-divider.ts b/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-divider/mdc-deprecated-list-divider.ts new file mode 100644 index 00000000..0631827b --- /dev/null +++ b/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-divider/mdc-deprecated-list-divider.ts @@ -0,0 +1,18 @@ +import { customElement, useView, PLATFORM } from 'aurelia-framework'; +import { bindable } from 'aurelia-typed-observable-plugin'; + +/** + * Optional, for list divider element + * @selector mdc-deprecated-list-divider + */ +@useView(PLATFORM.moduleName('./mdc-deprecated-list-divider.html')) +@customElement('mdc-deprecated-list-divider') +export class MdcDeprecatedListDivider { + /** To make a divider match the padding of list items */ + @bindable.booleanAttr + padded: boolean; + + /** Increases the leading margin of the divider so that it does not intersect the avatar column */ + @bindable.booleanAttr + inset: boolean; + } diff --git a/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-group.ts b/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-group.ts new file mode 100644 index 00000000..3b179866 --- /dev/null +++ b/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-group.ts @@ -0,0 +1,25 @@ +import { inlineView, customElement, children, customAttribute, inject } from 'aurelia-framework'; + +/** + * Optional. Wrapper around two or more mdc-list elements to be grouped together. + * @selector mdc-deprecated-list-group + */ +@inlineView('') +@customElement('mdc-deprecated-list-group') +export class MdcDeprecatedListGroup { + @children('h1,h2,h3,h4,h5,h6') + headers: HTMLElement[]; + headersChanged() { + this.headers.forEach(x => x.classList.add('mdc-deprecated-list-group__subheader')); + } +} + +@inject(Element) +@customAttribute('mdc-deprecated-list-group-subheader') +export class MdcDeprecatedListGroupSubheader { + constructor(private root: HTMLElement) { } + + attached() { + this.root.classList.add('mdc-deprecated-list-group__subheader'); + } +} diff --git a/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-item-graphic.ts b/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-item-graphic.ts new file mode 100644 index 00000000..c41d832d --- /dev/null +++ b/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-item-graphic.ts @@ -0,0 +1,15 @@ +import { customAttribute, inject } from 'aurelia-framework'; + +/** + * Optional. The first tile in the row (in LTR languages, the first column of the list item). Typically an icon or image. + * @selector [mdc-deprecated-list-item-graphic] + */ +@inject(Element) +@customAttribute('mdc-deprecated-list-item-graphic') +export class MdcDeprecatedListItemGraphic { + constructor(private root: HTMLElement) { } + + attached() { + this.root.classList.add('mdc-deprecated-list-item__graphic'); + } +} diff --git a/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-item-meta.ts b/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-item-meta.ts new file mode 100644 index 00000000..a6983a72 --- /dev/null +++ b/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-item-meta.ts @@ -0,0 +1,15 @@ +import { customAttribute, inject } from 'aurelia-framework'; + +/** + * Optional. The last tile in the row (in LTR languages, the last column of the list item). Typically small text, icon. or image. + * @selector [mdc-deprecated-list-item-meta] + */ +@inject(Element) +@customAttribute('mdc-deprecated-list-item-meta') +export class MdcDeprecatedListItemMeta { + constructor(private root: HTMLElement) { } + + attached() { + this.root.classList.add('mdc-deprecated-list-item__meta'); + } +} diff --git a/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-item-primary-text.ts b/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-item-primary-text.ts new file mode 100644 index 00000000..d86c5b78 --- /dev/null +++ b/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-item-primary-text.ts @@ -0,0 +1,9 @@ +import { inlineView, customElement } from 'aurelia-framework'; + +/** + * Optional, primary text for the list item + * @selector mdc-deprecated-list-item-primary-text + */ +@inlineView('') +@customElement('mdc-deprecated-list-item-primary-text') +export class MdcDeprecatedListItemPrimaryText { } diff --git a/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-item-secondary-text.ts b/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-item-secondary-text.ts new file mode 100644 index 00000000..ebaedf13 --- /dev/null +++ b/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-item-secondary-text.ts @@ -0,0 +1,9 @@ +import { inlineView, customElement } from 'aurelia-framework'; + +/** + * Optional, secondary text for the list item. Displayed below the primary text. + * @selector mdc-deprecated-list-item-secondary-text + */ +@inlineView('') +@customElement('mdc-deprecated-list-item-secondary-text') +export class MdcDeprecatedListItemPrimaryText { } diff --git a/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-item/enhance-mdc-deprecated-list-item.ts b/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-item/enhance-mdc-deprecated-list-item.ts new file mode 100644 index 00000000..f4f2b401 --- /dev/null +++ b/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-item/enhance-mdc-deprecated-list-item.ts @@ -0,0 +1,11 @@ +import { viewEngineHooks } from 'aurelia-framework'; + +@viewEngineHooks +export class EnhanceMdcDeprecatedListItem { + beforeCompile(template: DocumentFragment) { + const actions = template.querySelectorAll('[mdc-deprecated-list-item]'); + for (const i of Array.from(actions)) { + i.setAttribute('as-element', 'mdc-deprecated-list-item'); + } + } +} diff --git a/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-item/mdc-deprecated-list-item.html b/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-item/mdc-deprecated-list-item.html new file mode 100644 index 00000000..408791c7 --- /dev/null +++ b/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-item/mdc-deprecated-list-item.html @@ -0,0 +1,8 @@ + diff --git a/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-item/mdc-deprecated-list-item.ts b/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-item/mdc-deprecated-list-item.ts new file mode 100644 index 00000000..d2fb97d1 --- /dev/null +++ b/packages/list/src/mdc-deprecated-list/mdc-deprecated-list-item/mdc-deprecated-list-item.ts @@ -0,0 +1,97 @@ +import { inject, useView, customElement, processContent, ViewCompiler, ViewResources } from 'aurelia-framework'; +import { PLATFORM } from 'aurelia-pal'; +import { bindable } from 'aurelia-typed-observable-plugin'; + +let listItemId = 0; + +const ENTER = 13; +const SPACE = 32; +const LIST_ITEM_ACTION = 'mdclistitem:action'; + +/** + * @selector mdc-deprecated-list-item + */ +@inject(Element) +@useView(PLATFORM.moduleName('./mdc-deprecated-list-item.html')) +@customElement('mdc-deprecated-list-item') +@processContent(MdcDeprecatedListItem.processContent) +export class MdcDeprecatedListItem { + constructor(public root: HTMLElement) { } + + static processContent(_viewCompiler: ViewCompiler, _resources: ViewResources, element: Element) { + const graphic = element.querySelector('mdc-checkbox:not([mdc-deprecated-list-item-meta]),[mdc-deprecated-list-item-graphic]'); + if (graphic) { + element.removeChild(graphic); + } + const meta = element.querySelector('[mdc-deprecated-list-item-meta]'); + if (meta) { + element.removeChild(meta); + } + const itemText = document.createElement('span'); + itemText.classList.add('mdc-deprecated-list-item__text'); + const children = [].slice.call(element.childNodes) as ChildNode[]; + for (let i = 0; i < children.length; ++i) { + itemText.appendChild(children[i]); + } + if (graphic) { + element.appendChild(graphic); + } + element.appendChild(itemText); + if (meta) { + element.appendChild(meta); + } + return true; + } + + id = ++listItemId; + + /** Disables the list item */ + @bindable.booleanAttr + disabled: boolean; + + /** Styles the row in an activated state */ + @bindable.booleanAttr + activated: boolean; + + /** Random data associated with the list item. Passed in events payload. */ + @bindable + value: unknown; + + /** Disables ripple effect */ + @bindable.booleanAttr + disableRipple: boolean; + + onKeydown(evt: KeyboardEvent) { + if ((evt.keyCode === ENTER || evt.keyCode === SPACE) && !this.disabled) { + this.root.dispatchEvent(new CustomEvent(LIST_ITEM_ACTION, { detail: { item: this, data: this.value }, bubbles: true })); + } + return true; + } + + onClick() { + if (!this.disabled) { + this.root.dispatchEvent(new CustomEvent(LIST_ITEM_ACTION, { detail: { item: this, data: this.value }, bubbles: true })); + } + return true; + } + +} + +/** @hidden */ +export interface IMdcListItemElement extends HTMLElement { + au: { + controller: { + viewModel: MdcDeprecatedListItem; + }; + }; +} + +export interface IMdcListActionEventDetail { + index: number; + data: unknown; +} + +/** @hidden */ +export interface IMdcListActionEvent extends Event { + detail: IMdcListActionEventDetail; +} diff --git a/packages/list/src/mdc-deprecated-list/mdc-deprecated-list.html b/packages/list/src/mdc-deprecated-list/mdc-deprecated-list.html new file mode 100644 index 00000000..f9e34fa3 --- /dev/null +++ b/packages/list/src/mdc-deprecated-list/mdc-deprecated-list.html @@ -0,0 +1,15 @@ + diff --git a/packages/list/src/mdc-deprecated-list/mdc-deprecated-list.ts b/packages/list/src/mdc-deprecated-list/mdc-deprecated-list.ts new file mode 100644 index 00000000..1014db17 --- /dev/null +++ b/packages/list/src/mdc-deprecated-list/mdc-deprecated-list.ts @@ -0,0 +1,337 @@ +import { MdcComponent } from '@aurelia-mdc-web/base'; +import { MDCListFoundation, MDCListAdapter, strings, MDCListIndex } from '@material/list'; +import { inject, useView, customElement, children } from 'aurelia-framework'; +import { PLATFORM } from 'aurelia-pal'; +import { closest, matches } from '@material/dom/ponyfill'; +import { bindable } from 'aurelia-typed-observable-plugin'; +import { MdcDeprecatedListItem, IMdcListItemElement, IMdcListActionEventDetail } from './mdc-deprecated-list-item/mdc-deprecated-list-item'; + +strings.ACTION_EVENT = strings.ACTION_EVENT.toLowerCase(); + +export const mdcListStrings = { + ITEMS_CHANGED: 'mdclist:itemschanged' +}; + +/** + * @selector mdc-list + * @emits mdclist:action | Indicates that a list item with the specified index has been activated + * @emits mdclist:itemschanged | Indicates that the list of items has changed + */ +@inject(Element) +@useView(PLATFORM.moduleName('./mdc-deprecated-list.html')) +@customElement('mdc-deprecated-list') +export class MdcDeprecatedList extends MdcComponent{ + + /** Increases the height of the row to give it greater visual separation from adjacent rows */ + @bindable.booleanAttr + twoLine: boolean; + + /** When enabled, the space and enter keys (or click event) will trigger an single list item to become selected and any other previous selected element to become deselected */ + @bindable.booleanAttr + singleSelection: boolean; + async singleSelectionChanged() { + await this.initialised; + this.foundation?.setSingleSelection(this.singleSelection); + } + + /** Sets the selection logic to apply/remove the mdc-list-item--activated class */ + @bindable.booleanAttr + activated: boolean; + async activatedChanged() { + await this.initialised; + this.foundation?.setUseActivatedClass(this.activated); + } + + /** Sets the list to an orientation causing the keys used for navigation to change. true results in the Up/Down arrow keys being used. If false, the Left/Right arrow keys are used. */ + @bindable.booleanAttr + vertical: boolean = true; + + /** Increases the density of the list, making it appear more compact */ + @bindable.booleanAttr + dense: boolean; + + /** Optional, configures lists that start with text */ + @bindable.booleanAttr + textual: boolean; + + /** Configures the leading tiles of each row to display images instead of icons. This will make the graphics of the list items larger. */ + @bindable.booleanAttr + avatar: boolean; + + /** Optional, configures the leading tile of each row to display icons */ + @bindable.booleanAttr + icon: boolean; + + /** Optional, configures the leading tile of each row to display images */ + @bindable.booleanAttr + image: boolean; + + /** Optional, configures the leading tile of each row to display smaller images (this is analogous to an avatar list but the image will not be rounded) */ + @bindable.booleanAttr + thumbnail: boolean; + + /** Optional, configures the leading tile of each row to display videos */ + @bindable.booleanAttr + video: boolean; + + @children('mdc-deprecated-list-item') + items: MdcDeprecatedListItem[]; + itemsChanged() { + this.foundation?.layout(); + this.emit(mdcListStrings.ITEMS_CHANGED, { items: this.items }, true); + } + + @bindable.booleanAttr + typeahead: boolean; + async typeaheadChanged(hasTypeahead: boolean) { + await this.initialised; + this.foundation?.setHasTypeahead(hasTypeahead); + } + + /** Sets the list to allow the up arrow on the first element to focus the last element of the list and vice versa */ + @bindable.booleanAttr + wrapFocus: boolean; + async wrapFocusChanged() { + await this.initialised; + this.foundation?.setWrapFocus(this.wrapFocus); + } + + initialSyncWithDOM() { + this.layout(); + this.initializeListType(); + } + + get listElements(): Element[] { + return Array.from(this.root.querySelectorAll('.mdc-deprecated-list-item')); + } + + /** + * Extracts the primary text from a list item. + * @param item The list item element. + * @return The primary text in the element. + */ + getPrimaryText(item: Element): string { + const primaryText = item.querySelector('.mdc-deprecated-list-item__primary-text'); + if (primaryText) { + return primaryText.textContent ?? ''; + } + + const singleLineText = item.querySelector('.mdc-deprecated-list-item__text'); + return singleLineText?.textContent ?? ''; + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + const adapter: MDCListAdapter = { + addClassForElementIndex: (index, className) => { + const element = this.listElements[index]; + if (element) { + element.classList.add(className); + } + }, + focusItemAtIndex: (index) => { + const element = this.listElements[index] as HTMLElement | undefined; + if (element) { + element.focus(); + } + }, + getAttributeForElementIndex: (index, attr) => this.listElements[index].getAttribute(attr), + getFocusedElementIndex: () => this.listElements.indexOf(document.activeElement!), + getListItemCount: () => this.listElements.length, + getPrimaryTextAtIndex: (index) => this.getPrimaryText(this.listElements[index]), + hasCheckboxAtIndex: (index) => { + const listItem = this.listElements[index]; + return !!listItem.querySelector(strings.CHECKBOX_SELECTOR); + }, + hasRadioAtIndex: (index) => { + const listItem = this.listElements[index]; + return !!listItem.querySelector(strings.RADIO_SELECTOR); + }, + isCheckboxCheckedAtIndex: (index) => { + const listItem = this.listElements[index]; + const toggleEl = listItem.querySelector(strings.CHECKBOX_SELECTOR); + return toggleEl!.checked; + }, + isFocusInsideList: () => { + return this.root !== document.activeElement && this.root.contains(document.activeElement); + }, + isRootFocused: () => document.activeElement === this.root, + listItemAtIndexHasClass: (index, className) => this.listElements[index].classList.contains(className), + notifyAction: (index) => { + const listItem = this.listElements[index]; + if (!listItem.hasAttribute('no-list-action')) { + const data = (listItem as IMdcListItemElement).au.controller.viewModel.value; + this.emit(strings.ACTION_EVENT, { index, data }, /** shouldBubble */ true); + } + }, + removeClassForElementIndex: (index, className) => { + const element = this.listElements[index]; + if (element) { + element.classList.remove(className); + } + }, + setAttributeForElementIndex: (index, attr, value) => { + const element = this.listElements[index]; + if (element) { + element.setAttribute(attr, value); + } + }, + setCheckedCheckboxOrRadioAtIndex: (index, isChecked) => { + const listItem = this.listElements[index]; + const toggleEl = listItem.querySelector(strings.CHECKBOX_RADIO_SELECTOR); + if (toggleEl?.disabled) { + return; + } + toggleEl!.checked = isChecked; + + const event = document.createEvent('Event'); + event.initEvent('change', true, true); + toggleEl!.dispatchEvent(event); + }, + setTabIndexForListItemChildren: (listItemIndex, tabIndexValue) => { + const element = this.listElements[listItemIndex]; + const listItemChildren: Element[] = + [].slice.call(element.querySelectorAll(strings.CHILD_ELEMENTS_TO_TOGGLE_TABINDEX)); + listItemChildren.forEach((el) => el.setAttribute('tabindex', tabIndexValue)); + }, + }; + return new MDCListFoundation(adapter); + } + + /** + * @hidden + * Used to figure out which list item this event is targetting. Or returns -1 if + * there is no list item + */ + private getListItemIndex_(evt: Event) { + const eventTarget = evt.target as Element; + const nearestParent = closest(eventTarget, '.mdc-deprecated-list-item, .mdc-deprecated-list'); + + // Get the index of the element if it is a list item. + if (nearestParent && matches(nearestParent, '.mdc-deprecated-list-item')) { + return this.listElements.indexOf(nearestParent); + } + + return -1; + } + + /** + * @hidden + * Used to figure out which element was clicked before sending the event to the foundation. + */ + handleFocusInEvent_(evt: FocusEvent) { + const index = this.getListItemIndex_(evt); + this.foundation?.handleFocusIn(index); + } + + /** + * @hidden + * Used to figure out which element was clicked before sending the event to the foundation. + */ + handleFocusOutEvent_(evt: FocusEvent) { + const index = this.getListItemIndex_(evt); + this.foundation?.handleFocusOut(index); + } + + /** + * @hidden + * Used to figure out which element was focused when keydown event occurred before sending the event to the + * foundation. + */ + handleKeydownEvent_(evt: KeyboardEvent) { + const index = this.getListItemIndex_(evt); + const target = evt.target as Element; + if (!target.hasAttribute('not-selectable')) { + this.foundation?.handleKeydown(evt, target.classList.contains('mdc-deprecated-list-item'), index); + } + return true; + } + + /** + * @hidden + * Used to figure out which element was clicked before sending the event to the foundation. + */ + handleClickEvent_(evt: MouseEvent) { + const index = this.getListItemIndex_(evt); + const target = evt.target as Element; + // Toggle the checkbox only if it's not the target of the event, or the checkbox will have 2 change events. + const toggleCheckbox = !matches(target, strings.CHECKBOX_RADIO_SELECTOR); + this.foundation?.handleClick(index, toggleCheckbox); + return true; + } + + /** + * @hidden + * @return Whether typeahead is currently matching a user-specified prefix. + */ + get typeaheadInProgress(): boolean { + return this.foundation!.isTypeaheadInProgress(); + } + + /** + * @hidden + * Given the next desired character from the user, adds it to the typeahead + * buffer. Then, attempts to find the next option matching the buffer. Wraps + * around if at the end of options. + * + * @param nextChar The next character to add to the prefix buffer. + * @param startingIndex The index from which to start matching. Defaults to + * the currently focused index. + * @return The index of the matched item. + */ + typeaheadMatchItem(nextChar: string, startingIndex?: number): number { + return this.foundation!.typeaheadMatchItem(nextChar, startingIndex, /** skipFocus */ true); + } + + layout() { + const direction = this.root.getAttribute(strings.ARIA_ORIENTATION); + this.vertical = direction !== strings.ARIA_ORIENTATION_HORIZONTAL; + + // List items need to have at least tabindex=-1 to be focusable. + [].slice.call(this.root.querySelectorAll('.mdc-deprecated-list-item:not([tabindex])')) + .forEach((el: Element) => { + el.setAttribute('tabindex', '-1'); + }); + + // Child button/a elements are not tabbable until the list item is focused. + [].slice.call(this.root.querySelectorAll(strings.FOCUSABLE_CHILD_ELEMENTS)) + .forEach((el: Element) => el.setAttribute('tabindex', '-1')); + + this.foundation?.layout(); + } + + get selectedIndex(): MDCListIndex { + return this.foundation!.getSelectedIndex(); + } + + set selectedIndex(index: MDCListIndex) { + this.foundation?.setSelectedIndex(index); + } + + /** + * @hidden + * Initialize selectedIndex value based on pre-selected checkbox list items, single selection or radio. + */ + initializeListType() { + const checkboxListItems = this.root.querySelectorAll(strings.ARIA_ROLE_CHECKBOX_SELECTOR); + const radioSelectedListItem = this.root.querySelector(strings.ARIA_CHECKED_RADIO_SELECTOR); + + if (checkboxListItems.length) { + const preselectedItems = this.root.querySelectorAll(strings.ARIA_CHECKED_CHECKBOX_SELECTOR); + this.selectedIndex = [].map.call(preselectedItems, (listItem: Element) => this.listElements.indexOf(listItem)) as number[]; + } else if (radioSelectedListItem) { + this.selectedIndex = this.listElements.indexOf(radioSelectedListItem); + } + } +} + +/** @hidden */ +export interface IMdcListElement extends HTMLElement { + au: { + controller: { + viewModel: MdcDeprecatedList; + }; + }; +} +