From 56dc6ddc34a11dccf2ddffd5fde9f91f43004ea9 Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Mon, 9 Dec 2019 14:26:14 +1100 Subject: [PATCH 001/161] first attempt at ponyfill --- demos/index.js | 13 +- demos/select.html | 71 +- packages/list/package.json | 22 + packages/list/src/mwc-list-item-base.ts | 16 + packages/list/src/mwc-list-item.scss | 18 + packages/list/src/mwc-list-item.ts | 16 + packages/list/tsconfig.json | 18 + packages/select/package.json | 33 + packages/select/src/mwc-list-item-ponyfill.ts | 70 ++ packages/select/src/mwc-list-ponyfill.ts | 103 ++ packages/select/src/mwc-menu-ponyfill.ts | 95 ++ .../select/src/mwc-menu-surface-directive.ts | 45 + packages/select/src/mwc-select-base.ts | 1079 +++++++++++++++++ packages/select/src/mwc-select.scss | 25 + packages/select/src/mwc-select.ts | 31 + packages/select/tsconfig.json | 35 + test/bench-runner.html | 2 +- test/tsconfig-node.json | 4 +- 18 files changed, 1641 insertions(+), 55 deletions(-) create mode 100644 packages/list/package.json create mode 100644 packages/list/src/mwc-list-item-base.ts create mode 100644 packages/list/src/mwc-list-item.scss create mode 100644 packages/list/src/mwc-list-item.ts create mode 100644 packages/list/tsconfig.json create mode 100644 packages/select/package.json create mode 100644 packages/select/src/mwc-list-item-ponyfill.ts create mode 100644 packages/select/src/mwc-list-ponyfill.ts create mode 100644 packages/select/src/mwc-menu-ponyfill.ts create mode 100644 packages/select/src/mwc-menu-surface-directive.ts create mode 100644 packages/select/src/mwc-select-base.ts create mode 100644 packages/select/src/mwc-select.scss create mode 100644 packages/select/src/mwc-select.ts create mode 100644 packages/select/tsconfig.json diff --git a/demos/index.js b/demos/index.js index d25904c7db..219fba6ac0 100644 --- a/demos/index.js +++ b/demos/index.js @@ -194,14 +194,21 @@ class DemoView extends LitElement { Text field - Single and multiline text fields + Single line text fields Text area - Single and multiline text areas + Multiline text areas + + + + + + Select + Single option dropdown select menus @@ -224,4 +231,4 @@ class DemoView extends LitElement { } } -customElements.define('demo-view', DemoView); \ No newline at end of file +customElements.define('demo-view', DemoView); diff --git a/demos/select.html b/demos/select.html index 757ea07985..df211e5efe 100644 --- a/demos/select.html +++ b/demos/select.html @@ -46,56 +46,29 @@
- - - - - - - - - - - -

box

- - - - - - - - - + +
  • +
  • + Bread, Cereal, Rice, and Pasta +
  • +
  • + Vegetables +
  • +
  • + Fruit +
  • - -

    box - sized

    - - - - - - - - - - - -

    disabled

    - - - - - - - - - - - - - - + +
  • +
  • + Bread, Cereal, Rice, and Pasta +
  • +
  • + Vegetables +
  • +
  • + Fruit +
  • \ No newline at end of file diff --git a/test/tsconfig-node.json b/test/tsconfig-node.json index ca7f79e842..7849a0c185 100644 --- a/test/tsconfig-node.json +++ b/test/tsconfig-node.json @@ -2,12 +2,12 @@ "extends": "../tsconfig.json", "compilerOptions": { "composite": true, - "outDir": "lib", + "outDir": "lib/test/src", "module": "commonjs", "tsBuildInfoFile": ".tsbuildinfo-node", "rootDir": "./src" }, "include": [ - "src/benchmark/cli.ts", + "src/benchmark/cli.ts" ] } \ No newline at end of file From 0338dc3096eb335c84dfe01641b306f37d06c575 Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Mon, 9 Dec 2019 15:45:25 +1100 Subject: [PATCH 002/161] fix floatLabel --- packages/select/src/mwc-select-base.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/select/src/mwc-select-base.ts b/packages/select/src/mwc-select-base.ts index d7bff3a8d0..3839f1074b 100644 --- a/packages/select/src/mwc-select-base.ts +++ b/packages/select/src/mwc-select-base.ts @@ -219,9 +219,9 @@ export abstract class SelectBase extends FormElement { hasLabel: () => { return !!this.label; }, - floatLabel: () => { + floatLabel: (shouldFloat) => { if (this.labelElement) { - this.labelElement.floatingLabelFoundation.float(true); + this.labelElement.floatingLabelFoundation.float(shouldFloat); } }, getLabelWidth: () => { @@ -882,7 +882,6 @@ export abstract class SelectBase extends FormElement { } const anchorElement = mwcMenu.anchorElement(menuElement); - debugger; return anchorElement ? anchorElement.getBoundingClientRect() : null; }, From 2955d4ce1a02254fc79c582ecb4058a02deaf46f Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Mon, 9 Dec 2019 15:46:50 +1100 Subject: [PATCH 003/161] run formatter --- packages/select/src/mwc-list-item-ponyfill.ts | 104 ++++++------ packages/select/src/mwc-list-ponyfill.ts | 148 ++++++++++-------- packages/select/src/mwc-menu-ponyfill.ts | 131 +++++++++------- .../select/src/mwc-menu-surface-directive.ts | 10 +- packages/select/src/mwc-select-base.ts | 81 +++++----- 5 files changed, 263 insertions(+), 211 deletions(-) diff --git a/packages/select/src/mwc-list-item-ponyfill.ts b/packages/select/src/mwc-list-item-ponyfill.ts index 8f14a08672..22e256f8d6 100644 --- a/packages/select/src/mwc-list-item-ponyfill.ts +++ b/packages/select/src/mwc-list-item-ponyfill.ts @@ -24,47 +24,63 @@ export const text = (listItem: Element) => { return textContent ? textContent.trim() : ''; }; -export const value = (listItem: Element) => { - return listItem.getAttribute('data-value') || ''; -} - -export const controlTabIndex = (listItem: Element, tabIndex: string) => { - const tabbables = listItem.querySelectorAll('button:not(:disabled),a,.radio:not([disabled]),.checkbox:not([disabled]),.tabbable:not([disabled])'); - tabbables.forEach(element => element.setAttribute('tabindex', tabIndex)); -} - -const getCheckbox = (listItem: Element) => { - return listItem.querySelector('input[type="checkbox"]:not(:disabled),.checkbox:not([disabled])') as Element | HasChecked | null; -} - -const getRadio = (listItem: Element) => { - return listItem.querySelector('input[type="radio"]:not(:disabled),.radio:not([disabled])') as Element | HasChecked | null; -} - -export const hasCheckbox = (listItem: Element) => { - return !!getCheckbox(listItem); -} - -export const hasRadio = (listItem: Element) => { - return !!getRadio(listItem); -} - -export const isChecked = (listItem: Element) => { - const checkbox = getCheckbox(listItem); - return checkbox && 'checked' in checkbox ? checkbox.checked : false; -} - -export const setChecked = (listItem:Element, isChecked: boolean) => { - const checkbox = getCheckbox(listItem); - const radio = getRadio(listItem); - - if (checkbox && 'checked' in checkbox) { - checkbox.checked = isChecked; - } - - if (radio && 'checked' in radio) { - radio.checked = isChecked; - } -} - -export const hasClass = (listItem: Element, className: string) => listItem.classList.contains(className); \ No newline at end of file +export const value = + (listItem: Element) => { + return listItem.getAttribute('data-value') || ''; + } + +export const controlTabIndex = + (listItem: Element, tabIndex: string) => { + const tabbables = listItem.querySelectorAll( + 'button:not(:disabled),a,.radio:not([disabled]),.checkbox:not([disabled]),.tabbable:not([disabled])'); + tabbables.forEach(element => element.setAttribute('tabindex', tabIndex)); + } + +const getCheckbox = + (listItem: Element) => { + return listItem.querySelector( + 'input[type="checkbox"]:not(:disabled),.checkbox:not([disabled])') as + Element | + HasChecked | null; + } + +const getRadio = + (listItem: Element) => { + return listItem.querySelector( + 'input[type="radio"]:not(:disabled),.radio:not([disabled])') as + Element | + HasChecked | null; + } + +export const hasCheckbox = + (listItem: Element) => { + return !!getCheckbox(listItem); + } + +export const hasRadio = + (listItem: Element) => { + return !!getRadio(listItem); + } + +export const isChecked = + (listItem: Element) => { + const checkbox = getCheckbox(listItem); + return checkbox && 'checked' in checkbox ? checkbox.checked : false; + } + +export const setChecked = + (listItem: Element, isChecked: boolean) => { + const checkbox = getCheckbox(listItem); + const radio = getRadio(listItem); + + if (checkbox && 'checked' in checkbox) { + checkbox.checked = isChecked; + } + + if (radio && 'checked' in radio) { + radio.checked = isChecked; + } + } + +export const hasClass = (listItem: Element, className: string) => + listItem.classList.contains(className); \ No newline at end of file diff --git a/packages/select/src/mwc-list-ponyfill.ts b/packages/select/src/mwc-list-ponyfill.ts index 4573c66f5d..fb4f1129c6 100644 --- a/packages/select/src/mwc-list-ponyfill.ts +++ b/packages/select/src/mwc-list-ponyfill.ts @@ -16,88 +16,102 @@ */ import MDCListFoundation from '@material/list/foundation'; -export const selected = (list: Element) => { - const children = assignedElements(list); - for (const child of children) { - const selected = child.querySelector('.mdc-list-item--selected'); - if (selected) { - return selected; +export const selected = + (list: Element) => { + const children = assignedElements(list); + for (const child of children) { + const selected = child.querySelector('.mdc-list-item--selected'); + if (selected) { + return selected; + } + } + + return null; } - } - return null; -} +export const select = + (list: Element, itemToSelect: Element) => { + const previouslySelected = selected(list); -export const select = (list: Element, itemToSelect: Element) => { - const previouslySelected = selected(list); + if (previouslySelected) { + previouslySelected.classList.remove('mdc-list-item--selected'); + previouslySelected.removeAttribute('aria-selected'); + } - if (previouslySelected) { - previouslySelected.classList.remove('mdc-list-item--selected'); - previouslySelected.removeAttribute('aria-selected'); - } - - itemToSelect.classList.add('mdc-list-item--selected'); - itemToSelect.removeAttribute('aria-selected'); -} - -export const assignedElements = (list: Element): Element[] => { - const slot = list.querySelector('slot'); + itemToSelect.classList.add('mdc-list-item--selected'); + itemToSelect.removeAttribute('aria-selected'); + } - if (slot) { - return slot.assignedNodes({flatten: true}).filter(node => (node instanceof Element)) as Element[]; - } +export const assignedElements = (list: Element): + Element[] => { + const slot = list.querySelector('slot'); - return []; -} + if (slot) { + return slot.assignedNodes({flatten: true}) + .filter(node => (node instanceof Element)) as Element[]; + } -export const listElements = (list: Element): Element[] => { - const nodes = assignedElements(list); - const listItems = nodes.map((element) => { - if (element.classList.contains('mdc-list-item')) { - return element; + return []; } - return Array.from(element.querySelectorAll('.mdc-list-item')); - }).reduce((listItems, listItemResult) => { - return listItems.concat(listItemResult); - }, []); - - return listItems; -} +export const listElements = (list: Element): + Element[] => { + const nodes = assignedElements(list); + const listItems = + nodes + .map((element) => { + if (element.classList.contains('mdc-list-item')) { + return element; + } + + return Array.from(element.querySelectorAll('.mdc-list-item')); + }) + .reduce((listItems, listItemResult) => { + return listItems.concat(listItemResult); + }, []); + + return listItems; + } -export const mdcRoot = (list: Element) => { - return list.querySelector('.mdc-select') as HTMLElement; -} +export const mdcRoot = + (list: Element) => { + return list.querySelector('.mdc-select') as HTMLElement; + } -export const getSlottedActiveElement = (list: Element): Element|null => { - const first = getElementAtIndex(list, 0); - if (!first) { - return null; - } +export const getSlottedActiveElement = (list: Element): + Element|null => { + const first = getElementAtIndex(list, 0); + if (!first) { + return null; + } - const root = first.getRootNode() as unknown as DocumentOrShadowRoot; - return root ? root.activeElement : null; -} + const root = first.getRootNode() as unknown as DocumentOrShadowRoot; + return root ? root.activeElement : null; + } -export const doContentsHaveFocus = (list: Element): boolean => { - const activeElement = getSlottedActiveElement(list); - if (!activeElement) { - return false - } +export const doContentsHaveFocus = (list: Element): + boolean => { + const activeElement = getSlottedActiveElement(list); + if (!activeElement) { + return false + } - const elements = assignedElements(list); + const elements = assignedElements(list); - return elements.reduce((isContained: boolean, listItem) => { - return isContained || listItem === activeElement || listItem.contains(activeElement); - }, false); -} + return elements.reduce((isContained: boolean, listItem) => { + return isContained || listItem === activeElement || + listItem.contains(activeElement); + }, false); + } -export const getElementAtIndex = (list: Element, index: number): Element|undefined => { - const elements = listElements(list); +export const getElementAtIndex = (list: Element, index: number): + Element|undefined => { + const elements = listElements(list); - return elements[index] as Element | undefined; -} + return elements[index] as Element | undefined; + } -export const wrapFocus = (foundation: MDCListFoundation, wrapFocus: boolean) => { - foundation.setWrapFocus(wrapFocus); -} \ No newline at end of file +export const wrapFocus = + (foundation: MDCListFoundation, wrapFocus: boolean) => { + foundation.setWrapFocus(wrapFocus); + } \ No newline at end of file diff --git a/packages/select/src/mwc-menu-ponyfill.ts b/packages/select/src/mwc-menu-ponyfill.ts index 0d7981d5f0..5721277ee2 100644 --- a/packages/select/src/mwc-menu-ponyfill.ts +++ b/packages/select/src/mwc-menu-ponyfill.ts @@ -14,82 +14,99 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ +import {Corner} from '@material/menu-surface/constants'; import MDCMenuSurfaceFoundation from '@material/menu-surface/foundation'; -import { getTransformPropertyName } from '@material/menu-surface/util'; -import { Corner } from '@material/menu-surface/constants'; +import {getTransformPropertyName} from '@material/menu-surface/util'; -export const open = (foundation: MDCMenuSurfaceFoundation) => { - foundation.open(); -} +export const open = + (foundation: MDCMenuSurfaceFoundation) => { + foundation.open(); + } -export const close = (foundation: MDCMenuSurfaceFoundation) => { - foundation.close(); -} +export const close = + (foundation: MDCMenuSurfaceFoundation) => { + foundation.close(); + } -export const anchorElement = (menu: Element) => { - const menuAnchorable = menu as AnchorableElement; +export const anchorElement = + (menu: Element) => { + const menuAnchorable = menu as AnchorableElement; - return menuAnchorable.anchorElement; -} + return menuAnchorable.anchorElement; + } -const assignedElements = (menu: Element): Element[] => { - const slot = menu.querySelector('slot'); +const assignedElements = (menu: Element): + Element[] => { + const slot = menu.querySelector('slot'); - if (slot) { - return slot.assignedNodes({flatten: true}).filter(node => (node instanceof Element)) as Element[]; - } + if (slot) { + return slot.assignedNodes({flatten: true}) + .filter(node => (node instanceof Element)) as Element[]; + } - return []; -} + return []; + } -export const isElementInMenu = (menu: Element, element: Element): boolean => { - const assigned = assignedElements(menu); +export const isElementInMenu = (menu: Element, element: Element): + boolean => { + const assigned = assignedElements(menu); - return assigned.reduce((isContained: boolean, assigned) => { - return isContained || assigned === element || assigned.contains(element); - }, false); -} + return assigned.reduce((isContained: boolean, assigned) => { + return isContained || assigned === element || + assigned.contains(element); + }, false); + } -export const setTransformOrigin = (menu: HTMLElement, origin: string) => { - const propertyName = `${getTransformPropertyName(window)}-origin`; - menu.style.setProperty(propertyName, origin); -} +export const setTransformOrigin = + (menu: HTMLElement, origin: string) => { + const propertyName = `${getTransformPropertyName(window)}-origin`; + menu.style.setProperty(propertyName, origin); + } -export const mdcRoot = (menu: Element) => { - return menu.querySelector('.mdc-menu'); -} +export const mdcRoot = + (menu: Element) => { + return menu.querySelector('.mdc-menu'); + } -export const getDeepFocus = () => { - let activeElement = document.activeElement; +export const getDeepFocus = + () => { + let activeElement = document.activeElement; - if (!activeElement) { - return null; - } + if (!activeElement) { + return null; + } - while (activeElement) { - if (activeElement.shadowRoot) { - activeElement = activeElement.shadowRoot.activeElement; - } else { - break; - } - } + while (activeElement) { + if (activeElement.shadowRoot) { + activeElement = activeElement.shadowRoot.activeElement; + } else { + break; + } + } - return activeElement; -} + return activeElement; + } -export const setPreviousFocus = (menu: Element, previouslyFocused: Element|null) => { - (menu as Element & {_previousFocus: HTMLElement | Element | null})._previousFocus = previouslyFocused; -} +export const setPreviousFocus = + (menu: Element, previouslyFocused: Element|null) => { + (menu as Element & { + _previousFocus: HTMLElement | Element | null + })._previousFocus = previouslyFocused; + } -export const getPreviousFocus = (menu: Element) => { - return (menu as Element & {_previousFocus: HTMLElement | Element | null})._previousFocus; -} +export const getPreviousFocus = + (menu: Element) => { + return (menu as Element & {_previousFocus: HTMLElement | Element | null}) + ._previousFocus; + } -export const shadowRoot = (menu: Element) => menu.getRootNode() as ShadowRoot | Document; +export const shadowRoot = (menu: Element) => + menu.getRootNode() as ShadowRoot | Document; -export type AnchorableElement = HTMLElement & {anchorElement: Element | null}; +export type AnchorableElement = HTMLElement&{anchorElement: Element | null}; -export const setAnchorCorner = (foundation: MDCMenuSurfaceFoundation, corner: Corner) => { - foundation.setAnchorCorner(corner); -} \ No newline at end of file +export const setAnchorCorner = + (foundation: MDCMenuSurfaceFoundation, corner: Corner) => { + foundation.setAnchorCorner(corner); + } \ No newline at end of file diff --git a/packages/select/src/mwc-menu-surface-directive.ts b/packages/select/src/mwc-menu-surface-directive.ts index 7b7433bf10..2817a2fa75 100644 --- a/packages/select/src/mwc-menu-surface-directive.ts +++ b/packages/select/src/mwc-menu-surface-directive.ts @@ -15,21 +15,21 @@ See the License for the specific language governing permissions and limitations under the License. */ import {directive, PropertyPart} from 'lit-html'; -import { AnchorableElement } from './mwc-menu-ponyfill'; +import {AnchorableElement} from './mwc-menu-ponyfill'; export interface MenuAnchor extends HTMLElement { - anchoring: Element | null; + anchoring: Element|null; } -const partToTarget = - new WeakMap(); +const partToTarget = new WeakMap(); export const menuAnchor = directive((forSelector: string) => (part: PropertyPart) => { const lastTarget = partToTarget.get(part); const anchorElement = part.committer.element; const root = anchorElement.getRootNode() as Document | ShadowRoot; - const target = root.querySelector(forSelector) as AnchorableElement | null; + const target = + root.querySelector(forSelector) as AnchorableElement | null; if (target !== lastTarget) { anchorElement.classList.add('mdc-menu-anchor'); diff --git a/packages/select/src/mwc-select-base.ts b/packages/select/src/mwc-select-base.ts index 3839f1074b..b90243e524 100644 --- a/packages/select/src/mwc-select-base.ts +++ b/packages/select/src/mwc-select-base.ts @@ -19,25 +19,25 @@ import '@material/mwc-notched-outline'; import {closest} from '@material/dom/ponyfill'; import {MDCFloatingLabelFoundation} from '@material/floating-label/foundation.js'; import {MDCLineRippleFoundation} from '@material/line-ripple/foundation.js'; -import {addHasRemoveClass, FormElement} from '@material/mwc-base/form-element.js'; -import MDCSelectFoundation from '@material/select/foundation.js'; -import MDCMenuFoundation from '@material/menu/foundation.js'; -import MDCMenuSurfaceFoundation from '@material/menu-surface/foundation.js'; +import {MDCListAdapter} from '@material/list/adapter'; import MDCListFoundation from '@material/list/foundation.js'; -import {html, query, property, TemplateResult, PropertyValues} from 'lit-element'; -import { MDCSelectAdapter } from '@material/select/adapter'; -import {lineRipple, LineRipple} from '@material/mwc-line-ripple'; +import {MDCMenuSurfaceAdapter} from '@material/menu-surface/adapter'; +import MDCMenuSurfaceFoundation from '@material/menu-surface/foundation.js'; +import {MDCMenuAdapter} from '@material/menu/adapter'; +import MDCMenuFoundation from '@material/menu/foundation.js'; +import {addHasRemoveClass, FormElement} from '@material/mwc-base/form-element.js'; import {floatingLabel, FloatingLabel} from '@material/mwc-floating-label'; +import {lineRipple, LineRipple} from '@material/mwc-line-ripple'; import {NotchedOutline} from '@material/mwc-notched-outline'; -import { MDCMenuAdapter } from '@material/menu/adapter'; -import { MDCMenuSurfaceAdapter } from '@material/menu-surface/adapter'; +import {MDCSelectAdapter} from '@material/select/adapter'; +import MDCSelectFoundation from '@material/select/foundation.js'; +import {html, property, PropertyValues, query, TemplateResult} from 'lit-element'; import {classMap} from 'lit-html/directives/class-map'; -import * as mwcMenu from './mwc-menu-ponyfill'; -import * as mwcList from './mwc-list-ponyfill'; import * as mwcListItem from './mwc-list-item-ponyfill'; -import { MDCListAdapter } from '@material/list/adapter'; -import { menuAnchor } from './mwc-menu-surface-directive'; +import * as mwcList from './mwc-list-ponyfill'; +import * as mwcMenu from './mwc-menu-ponyfill'; +import {menuAnchor} from './mwc-menu-surface-directive'; // must be done to get past lit-analyzer checks declare global { @@ -78,11 +78,13 @@ export abstract class SelectBase extends FormElement { @query('.mdc-list') protected listElement!: HTMLDivElement|null; - @query('.mdc-select__selected-text') protected selectedTextElement!: HTMLDivElement | null; + @query('.mdc-select__selected-text') + protected selectedTextElement!: HTMLDivElement|null; - @query('.mdc-select__anchor') protected anchorElement!: HTMLDivElement | null; + @query('.mdc-select__anchor') protected anchorElement!: HTMLDivElement|null; - @property({type: Boolean, attribute: 'disabled', reflect: true}) disabled = false; + @property({type: Boolean, attribute: 'disabled', reflect: true}) + disabled = false; @property({type: Boolean}) outlined = false; @@ -100,7 +102,7 @@ export abstract class SelectBase extends FormElement { protected listeners = []; - render () { + render() { let outlinedOrUnderlined = html``; if (this.outlined) { @@ -252,7 +254,7 @@ export abstract class SelectBase extends FormElement { }, notifyChange: (value) => { this.value = value; - const ev = new Event('change', {bubbles:true}); + const ev = new Event('change', {bubbles: true}); this.dispatchEvent(ev); }, setSelectedText: (value) => this.selectedText = value, @@ -263,7 +265,8 @@ export abstract class SelectBase extends FormElement { return false; } - const rootNode = selectedTextElement.getRootNode() as ShadowRoot | Document; + const rootNode = + selectedTextElement.getRootNode() as ShadowRoot | Document; return rootNode.activeElement === selectedTextElement; }, @@ -656,7 +659,8 @@ export abstract class SelectBase extends FormElement { element.removeAttribute(attr); }, - elementContainsClass: (element, className) => element.classList.contains(className), + elementContainsClass: (element, className) => + element.classList.contains(className), closeSurface: () => { if (this.mdcMenuSurfaceFoundation) { mwcMenu.close(this.mdcMenuSurfaceFoundation); @@ -675,10 +679,7 @@ export abstract class SelectBase extends FormElement { } const init: CustomEventInit = {}; - init.detail = { - index: evtData.index, - item: evtData - }; + init.detail = {index: evtData.index, item: evtData}; const ev = new CustomEvent('selected', init); this.menuElement.dispatchEvent(ev); }, @@ -710,19 +711,22 @@ export abstract class SelectBase extends FormElement { return -1; } - const elementAtIndex = mwcList.getElementAtIndex(this.listElement, index); + const elementAtIndex = + mwcList.getElementAtIndex(this.listElement, index); if (!elementAtIndex) { return -1; } - const selectionGroupEl = closest(elementAtIndex, `.mdc-menu__selection-group`); + const selectionGroupEl = + closest(elementAtIndex, `.mdc-menu__selection-group`); if (!selectionGroupEl) { return -1; } - const selectedItemEl = selectionGroupEl.querySelector(`.mdc-menu-item--selected`); + const selectedItemEl = + selectionGroupEl.querySelector(`.mdc-menu-item--selected`); if (!selectedItemEl) { return -1; @@ -731,14 +735,14 @@ export abstract class SelectBase extends FormElement { const elements = mwcList.listElements(this.listElement); return elements.indexOf(selectedItemEl); - }, isSelectableItemAtIndex: (index) => { if (!this.listElement) { return false; - } + } - const elementAtIndex = mwcList.getElementAtIndex(this.listElement, index); + const elementAtIndex = + mwcList.getElementAtIndex(this.listElement, index); if (!elementAtIndex) { return false; @@ -887,8 +891,7 @@ export abstract class SelectBase extends FormElement { }, getBodyDimensions: () => { return { - width: document.body.clientWidth, - height: document.body.clientHeight, + width: document.body.clientWidth, height: document.body.clientHeight, } }, getWindowDimensions: () => { @@ -919,7 +922,8 @@ export abstract class SelectBase extends FormElement { mdcRoot.style.left = 'left' in position ? `${position.left}px` : ''; mdcRoot.style.right = 'right' in position ? `${position.right}px` : ''; mdcRoot.style.top = 'top' in position ? `${position.top}px` : ''; - mdcRoot.style.bottom = 'bottom' in position ? `${position.bottom}px` : ''; + mdcRoot.style.bottom = + 'bottom' in position ? `${position.bottom}px` : ''; }, setMaxHeight: (height) => { const menuElement = this.menuElement; @@ -938,7 +942,8 @@ export abstract class SelectBase extends FormElement { }, }; - this.mdcMenuSurfaceFoundation = new MDCMenuSurfaceFoundation(mdcMenuSurfaceAdapter); + this.mdcMenuSurfaceFoundation = + new MDCMenuSurfaceFoundation(mdcMenuSurfaceAdapter); this.mdcMenuSurfaceFoundation.init(); } @@ -962,7 +967,8 @@ export abstract class SelectBase extends FormElement { // } // if (this.selectedText_.hasAttribute(strings.ARIA_CONTROLS)) { - // const helperTextElement = document.getElementById(this.selectedText_.getAttribute(strings.ARIA_CONTROLS)!); + // const helperTextElement = + // document.getElementById(this.selectedText_.getAttribute(strings.ARIA_CONTROLS)!); // if (helperTextElement) { // this.helperText_ = helperTextFactory(helperTextElement); // } @@ -973,7 +979,6 @@ export abstract class SelectBase extends FormElement { disconnectedCallback() { super.disconnectedCallback(); - } focus() { @@ -1008,7 +1013,7 @@ export abstract class SelectBase extends FormElement { } } - protected onClick(evt: MouseEvent | TouchEvent) { + protected onClick(evt: MouseEvent|TouchEvent) { if (this.mdcFoundation) { this.focus(); const targetClientRect = (evt.target as Element).getBoundingClientRect(); @@ -1031,7 +1036,7 @@ export abstract class SelectBase extends FormElement { } } - protected onSelected(evt: CustomEvent<{index:number}>) { + protected onSelected(evt: CustomEvent<{index: number}>) { if (this.mdcFoundation) { this.mdcFoundation.handleMenuItemAction(evt.detail.index); } From fbdce5ba3b953393bc2c93df7055182a74e20435 Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Mon, 9 Dec 2019 16:31:17 +1100 Subject: [PATCH 004/161] menu surface listeners --- packages/select/src/mwc-select-base.ts | 36 ++++++++++++++++++-------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/packages/select/src/mwc-select-base.ts b/packages/select/src/mwc-select-base.ts index b90243e524..efa1e71642 100644 --- a/packages/select/src/mwc-select-base.ts +++ b/packages/select/src/mwc-select-base.ts @@ -127,6 +127,7 @@ export abstract class SelectBase extends FormElement {
    + @closed=${this.onClosed} + @keydown=${this.onMenuSurfaceKeydown} + @opened=${this.registerBodyClick} + @closed=${this.deregisterBodyClick}>
    @@ -954,6 +958,26 @@ export abstract class SelectBase extends FormElement { } } + protected onMenuSurfaceKeydown(evt: KeyboardEvent) { + if (this.mdcMenuSurfaceFoundation) { + this.mdcMenuSurfaceFoundation.handleKeydown(evt) + } + } + + protected onBodyClick(evt: MouseEvent) { + if (this.mdcMenuSurfaceFoundation) { + this.mdcMenuSurfaceFoundation.handleBodyClick(evt); + } + } + + protected registerBodyClick() { + document.body.addEventListener('click', this.onBodyClick); + } + + protected deregisterBodyClick() { + document.body.removeEventListener('click', this.onBodyClick); + } + async firstUpdated() { const outlineElement = this.outlineElement; if (outlineElement) { @@ -965,16 +989,6 @@ export abstract class SelectBase extends FormElement { // if (this.validateOnInitialRender) { // this.reportValidity(); // } - - // if (this.selectedText_.hasAttribute(strings.ARIA_CONTROLS)) { - // const helperTextElement = - // document.getElementById(this.selectedText_.getAttribute(strings.ARIA_CONTROLS)!); - // if (helperTextElement) { - // this.helperText_ = helperTextFactory(helperTextElement); - // } - // } - - // this.menu_ = new MdcMenu(this.menuElement_); } disconnectedCallback() { From 789d050545d9683f873b192eb5b6c52c5398d0ef Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Mon, 9 Dec 2019 16:48:22 +1100 Subject: [PATCH 005/161] menu listeners --- packages/select/src/mwc-select-base.ts | 40 ++++++++++++++++++++------ 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/select/src/mwc-select-base.ts b/packages/select/src/mwc-select-base.ts index efa1e71642..c03b3b5f96 100644 --- a/packages/select/src/mwc-select-base.ts +++ b/packages/select/src/mwc-select-base.ts @@ -143,11 +143,14 @@ export abstract class SelectBase extends FormElement { role="listbox" class="mdc-select__menu mdc-menu mdc-menu-surface" @selected=${this.onSelected} + @keydown=${this.menuSurfaceOnKeydown} + @opened=${this.menuSurfaceRegisterBodyClick} + @closed=${this.menuSurfaceDeregisterBodyClick} + @opened=${this.menuOnOpened} + @keydown=${this.menuOnKeydown} + @action=${this.menuOnAction} @opened=${this.onOpened} - @closed=${this.onClosed} - @keydown=${this.onMenuSurfaceKeydown} - @opened=${this.registerBodyClick} - @closed=${this.deregisterBodyClick}> + @closed=${this.onClosed}>
    @@ -561,7 +564,7 @@ export abstract class SelectBase extends FormElement { return; } - const init: CustomEventInit = {}; + const init: CustomEventInit = {bubbles:true}; init.detail = {index}; const ev = new CustomEvent('action', init); this.listElement.dispatchEvent(ev); @@ -958,7 +961,7 @@ export abstract class SelectBase extends FormElement { } } - protected onMenuSurfaceKeydown(evt: KeyboardEvent) { + protected menuSurfaceOnKeydown(evt: KeyboardEvent) { if (this.mdcMenuSurfaceFoundation) { this.mdcMenuSurfaceFoundation.handleKeydown(evt) } @@ -970,14 +973,35 @@ export abstract class SelectBase extends FormElement { } } - protected registerBodyClick() { + protected menuSurfaceRegisterBodyClick() { document.body.addEventListener('click', this.onBodyClick); } - protected deregisterBodyClick() { + protected menuSurfaceDeregisterBodyClick() { document.body.removeEventListener('click', this.onBodyClick); } + protected menuOnKeydown(evt: KeyboardEvent) { + if (this.mdcMenuFoundation) { + this.mdcMenuFoundation.handleKeydown(evt); + } + } + + protected menuOnAction(evt: CustomEvent<{index: number}>) { + if (this.mdcMenuFoundation) { + const el = mwcList.getElementAtIndex(this.listElement!, evt.detail.index); + if (el) { + this.mdcMenuFoundation.handleItemAction(el); + } + } + } + + menuOnOpened() { + if (this.mdcMenuFoundation) { + this.mdcMenuFoundation.handleMenuSurfaceOpened(); + } + } + async firstUpdated() { const outlineElement = this.outlineElement; if (outlineElement) { From 03aeed80069123c3dbb02ad70ad776ed9505170b Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Tue, 10 Dec 2019 13:17:18 +1100 Subject: [PATCH 006/161] formatter --- packages/select/src/mwc-list-ponyfill.ts | 190 ++++++++++++----------- packages/select/src/mwc-menu-ponyfill.ts | 113 +++++++------- packages/select/src/util.ts | 3 + 3 files changed, 154 insertions(+), 152 deletions(-) create mode 100644 packages/select/src/util.ts diff --git a/packages/select/src/mwc-list-ponyfill.ts b/packages/select/src/mwc-list-ponyfill.ts index fb4f1129c6..7c2bfd3f86 100644 --- a/packages/select/src/mwc-list-ponyfill.ts +++ b/packages/select/src/mwc-list-ponyfill.ts @@ -16,102 +16,110 @@ */ import MDCListFoundation from '@material/list/foundation'; -export const selected = - (list: Element) => { - const children = assignedElements(list); - for (const child of children) { - const selected = child.querySelector('.mdc-list-item--selected'); - if (selected) { - return selected; - } - } - - return null; +export const selected = (list: Element) => { + const children = assignedElements(list); + for (const child of children) { + const selected = child.querySelector('.mdc-list-item--selected'); + if (selected) { + return selected; } - -export const select = - (list: Element, itemToSelect: Element) => { - const previouslySelected = selected(list); - - if (previouslySelected) { - previouslySelected.classList.remove('mdc-list-item--selected'); - previouslySelected.removeAttribute('aria-selected'); - } - - itemToSelect.classList.add('mdc-list-item--selected'); - itemToSelect.removeAttribute('aria-selected'); - } - -export const assignedElements = (list: Element): - Element[] => { - const slot = list.querySelector('slot'); - - if (slot) { - return slot.assignedNodes({flatten: true}) - .filter(node => (node instanceof Element)) as Element[]; - } - - return []; - } - -export const listElements = (list: Element): - Element[] => { - const nodes = assignedElements(list); - const listItems = - nodes - .map((element) => { - if (element.classList.contains('mdc-list-item')) { - return element; - } - - return Array.from(element.querySelectorAll('.mdc-list-item')); - }) - .reduce((listItems, listItemResult) => { - return listItems.concat(listItemResult); - }, []); - - return listItems; - } - -export const mdcRoot = - (list: Element) => { - return list.querySelector('.mdc-select') as HTMLElement; - } - -export const getSlottedActiveElement = (list: Element): - Element|null => { - const first = getElementAtIndex(list, 0); - if (!first) { - return null; - } - - const root = first.getRootNode() as unknown as DocumentOrShadowRoot; - return root ? root.activeElement : null; - } - -export const doContentsHaveFocus = (list: Element): - boolean => { - const activeElement = getSlottedActiveElement(list); - if (!activeElement) { - return false - } - - const elements = assignedElements(list); - - return elements.reduce((isContained: boolean, listItem) => { - return isContained || listItem === activeElement || - listItem.contains(activeElement); - }, false); - } - -export const getElementAtIndex = (list: Element, index: number): - Element|undefined => { + } + + return null; +}; + +export const select = (list: Element, itemToSelect: Element) => { + const previouslySelected = selected(list); + + if (previouslySelected) { + previouslySelected.classList.remove('mdc-list-item--selected'); + previouslySelected.removeAttribute('aria-selected'); + } + + itemToSelect.classList.add('mdc-list-item--selected'); + itemToSelect.removeAttribute('aria-selected'); +}; + +export const assignedElements = (list: Element): Element[] => { + const slot = list.querySelector('slot'); + + if (slot) { + return slot.assignedNodes({flatten: true}) + .filter(node => (node.nodeType === Node.ELEMENT_NODE)) as + Element[]; + } + + return []; +}; + +export const listElements = (list: Element): Element[] => { + const nodes = assignedElements(list); + const listItems = + nodes + .map((element) => { + if (element.classList.contains('mdc-list-item')) { + return element; + } + + return Array.from(element.querySelectorAll('.mdc-list-item')); + }) + .reduce((listItems, listItemResult) => { + return listItems.concat(listItemResult); + }, []); + + return listItems; +}; + +export const mdcRoot = (list: Element) => { + return list as HTMLElement; +}; + +export const getSlottedActiveElement = (list: Element): Element|null => { + const first = getElementAtIndex(list, 0); + if (!first) { + return null; + } + + const root = first.getRootNode() as unknown as DocumentOrShadowRoot; + return root ? root.activeElement : null; +}; + +export const doContentsHaveFocus = (list: Element): boolean => { + const activeElement = getSlottedActiveElement(list); + if (!activeElement) { + return false + } + + const elements = assignedElements(list); + + return elements.reduce((isContained: boolean, listItem) => { + return isContained || listItem === activeElement || + listItem.contains(activeElement); + }, false); +}; + +export const getElementAtIndex = + (list: Element, index: number): Element|undefined => { const elements = listElements(list); return elements[index] as Element | undefined; - } + }; export const wrapFocus = (foundation: MDCListFoundation, wrapFocus: boolean) => { foundation.setWrapFocus(wrapFocus); - } \ No newline at end of file + }; + +export const getIndexOfTarget = (list: Element, evt: Event) => { + const elements = listElements(list); + const path = evt.composedPath(); + + for (const pathItem of path) { + const index = elements.indexOf(pathItem as Element); + if (index !== -1) { + return index; + } + } + + return -1; +}; \ No newline at end of file diff --git a/packages/select/src/mwc-menu-ponyfill.ts b/packages/select/src/mwc-menu-ponyfill.ts index 5721277ee2..049e9e2772 100644 --- a/packages/select/src/mwc-menu-ponyfill.ts +++ b/packages/select/src/mwc-menu-ponyfill.ts @@ -17,89 +17,80 @@ limitations under the License. import {Corner} from '@material/menu-surface/constants'; import MDCMenuSurfaceFoundation from '@material/menu-surface/foundation'; import {getTransformPropertyName} from '@material/menu-surface/util'; +import {isElement} from './util'; -export const open = - (foundation: MDCMenuSurfaceFoundation) => { - foundation.open(); - } +export const open = (foundation: MDCMenuSurfaceFoundation) => { + foundation.open(); +}; -export const close = - (foundation: MDCMenuSurfaceFoundation) => { - foundation.close(); - } +export const close = (foundation: MDCMenuSurfaceFoundation) => { + foundation.close(); +}; -export const anchorElement = - (menu: Element) => { - const menuAnchorable = menu as AnchorableElement; +export const anchorElement = (menu: Element) => { + const menuAnchorable = menu as AnchorableElement; - return menuAnchorable.anchorElement; - } + return menuAnchorable.anchorElement; +}; -const assignedElements = (menu: Element): - Element[] => { - const slot = menu.querySelector('slot'); +const assignedElements = (menu: Element): Element[] => { + const slot = menu.querySelector('slot'); - if (slot) { - return slot.assignedNodes({flatten: true}) - .filter(node => (node instanceof Element)) as Element[]; - } + if (slot) { + return slot.assignedNodes({flatten: true}) + .filter(node => (isElement(node))) as Element[]; + } - return []; - } + return []; +}; -export const isElementInMenu = (menu: Element, element: Element): - boolean => { - const assigned = assignedElements(menu); +export const isElementInMenu = (menu: Element, element: Element): boolean => { + const assigned = assignedElements(menu); - return assigned.reduce((isContained: boolean, assigned) => { - return isContained || assigned === element || - assigned.contains(element); - }, false); - } + return assigned.reduce((isContained: boolean, assigned) => { + return isContained || assigned === element || assigned.contains(element); + }, false); +}; -export const setTransformOrigin = - (menu: HTMLElement, origin: string) => { - const propertyName = `${getTransformPropertyName(window)}-origin`; - menu.style.setProperty(propertyName, origin); - } +export const setTransformOrigin = (menu: HTMLElement, origin: string) => { + const propertyName = `${getTransformPropertyName(window)}-origin`; + menu.style.setProperty(propertyName, origin); +}; -export const mdcRoot = - (menu: Element) => { - return menu.querySelector('.mdc-menu'); - } - -export const getDeepFocus = - () => { - let activeElement = document.activeElement; +export const mdcRoot = (menu: Element) => { + return menu.querySelector('.mdc-menu'); +}; - if (!activeElement) { - return null; - } +export const getDeepFocus = () => { + let activeElement = document.activeElement; - while (activeElement) { - if (activeElement.shadowRoot) { - activeElement = activeElement.shadowRoot.activeElement; - } else { - break; - } - } + if (!activeElement) { + return null; + } - return activeElement; + while (activeElement) { + if (activeElement.shadowRoot) { + activeElement = activeElement.shadowRoot.activeElement; + } else { + break; } + } + + return activeElement; +}; export const setPreviousFocus = (menu: Element, previouslyFocused: Element|null) => { (menu as Element & { _previousFocus: HTMLElement | Element | null })._previousFocus = previouslyFocused; - } + }; -export const getPreviousFocus = - (menu: Element) => { - return (menu as Element & {_previousFocus: HTMLElement | Element | null}) - ._previousFocus; - } +export const getPreviousFocus = (menu: Element) => { + return (menu as Element & {_previousFocus: HTMLElement | Element | null}) + ._previousFocus; +}; export const shadowRoot = (menu: Element) => menu.getRootNode() as ShadowRoot | Document; @@ -109,4 +100,4 @@ export type AnchorableElement = HTMLElement&{anchorElement: Element | null}; export const setAnchorCorner = (foundation: MDCMenuSurfaceFoundation, corner: Corner) => { foundation.setAnchorCorner(corner); - } \ No newline at end of file + }; \ No newline at end of file diff --git a/packages/select/src/util.ts b/packages/select/src/util.ts new file mode 100644 index 0000000000..33a4fd331a --- /dev/null +++ b/packages/select/src/util.ts @@ -0,0 +1,3 @@ +export const isElement = (node: Node) => { + return node.nodeType === Node.ELEMENT_NODE; +}; \ No newline at end of file From 31f7316d3cd11d0b44f19b3d16eadd74fcef469f Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Tue, 10 Dec 2019 13:27:00 +1100 Subject: [PATCH 007/161] fix event listenres --- packages/select/src/mwc-select-base.ts | 142 +++++++++++++++++++++---- 1 file changed, 119 insertions(+), 23 deletions(-) diff --git a/packages/select/src/mwc-select-base.ts b/packages/select/src/mwc-select-base.ts index c03b3b5f96..d1273b4231 100644 --- a/packages/select/src/mwc-select-base.ts +++ b/packages/select/src/mwc-select-base.ts @@ -38,6 +38,7 @@ import * as mwcListItem from './mwc-list-item-ponyfill'; import * as mwcList from './mwc-list-ponyfill'; import * as mwcMenu from './mwc-menu-ponyfill'; import {menuAnchor} from './mwc-menu-surface-directive'; +import {isElement} from './util'; // must be done to get past lit-analyzer checks declare global { @@ -100,7 +101,9 @@ export abstract class SelectBase extends FormElement { @property({type: String}) icon = ''; - protected listeners = []; + protected listeners: ({target: Element, name: string, cb: any})[] = []; + protected onBodyClickBound: (evt: MouseEvent) => void = () => {}; + protected _outlineUpdateComplete: null|Promise = null; render() { let outlinedOrUnderlined = html``; @@ -135,7 +138,7 @@ export abstract class SelectBase extends FormElement { @blur=${this.onBlur} @keydown=${this.onKeydown} @click=${this.onClick}> - ${this.selectedText} + ${this.selectedText}
    ${outlinedOrUnderlined} @@ -143,15 +146,13 @@ export abstract class SelectBase extends FormElement { role="listbox" class="mdc-select__menu mdc-menu mdc-menu-surface" @selected=${this.onSelected} - @keydown=${this.menuSurfaceOnKeydown} - @opened=${this.menuSurfaceRegisterBodyClick} - @closed=${this.menuSurfaceDeregisterBodyClick} - @opened=${this.menuOnOpened} - @keydown=${this.menuOnKeydown} - @action=${this.menuOnAction} - @opened=${this.onOpened} - @closed=${this.onClosed}> -
      + @action=${this.menuOnAction}> +
      @@ -306,7 +307,7 @@ export abstract class SelectBase extends FormElement { } }, getAnchorElement: () => this.anchorElement, - setMenuAnchorElement: () => {}, + setMenuAnchorElement: () => { /* Handled by anchor directive */ }, setMenuAnchorCorner: (anchorCorner) => { if (this.mdcMenuSurfaceFoundation) { mwcMenu.setAnchorCorner(this.mdcMenuSurfaceFoundation, anchorCorner); @@ -511,8 +512,8 @@ export abstract class SelectBase extends FormElement { } const element = mwcList.getElementAtIndex(this.listElement, index); - if (element && element instanceof HTMLElement) { - element.focus(); + if (element && isElement(element)) { + (element as HTMLElement).focus(); } }, setTabIndexForListItemChildren: (index, tabIndex) => { @@ -564,7 +565,7 @@ export abstract class SelectBase extends FormElement { return; } - const init: CustomEventInit = {bubbles:true}; + const init: CustomEventInit = {bubbles: true}; init.detail = {index}; const ev = new CustomEvent('action', init); this.listElement.dispatchEvent(ev); @@ -704,8 +705,8 @@ export abstract class SelectBase extends FormElement { const element = mwcList.getElementAtIndex(this.listElement, index); - if (element && element instanceof HTMLElement) { - element.focus(); + if (element && isElement(element)) { + (element as HTMLElement).focus(); } }, focusListRoot: () => { @@ -893,6 +894,7 @@ export abstract class SelectBase extends FormElement { } const anchorElement = mwcMenu.anchorElement(menuElement); + debugger; return anchorElement ? anchorElement.getBoundingClientRect() : null; }, @@ -974,11 +976,12 @@ export abstract class SelectBase extends FormElement { } protected menuSurfaceRegisterBodyClick() { - document.body.addEventListener('click', this.onBodyClick); + this.onBodyClickBound = this.onBodyClick.bind(this); + document.body.addEventListener('click', this.onBodyClickBound); } protected menuSurfaceDeregisterBodyClick() { - document.body.removeEventListener('click', this.onBodyClick); + document.body.removeEventListener('click', this.onBodyClickBound); } protected menuOnKeydown(evt: KeyboardEvent) { @@ -988,24 +991,66 @@ export abstract class SelectBase extends FormElement { } protected menuOnAction(evt: CustomEvent<{index: number}>) { - if (this.mdcMenuFoundation) { - const el = mwcList.getElementAtIndex(this.listElement!, evt.detail.index); + if (this.mdcMenuFoundation && this.listElement) { + const el = mwcList.getElementAtIndex(this.listElement, evt.detail.index); if (el) { this.mdcMenuFoundation.handleItemAction(el); } } } - menuOnOpened() { + protected menuOnOpened() { if (this.mdcMenuFoundation) { this.mdcMenuFoundation.handleMenuSurfaceOpened(); } } + private listOnFocusin(evt: FocusEvent) { + if (this.mdcListFoundation && this.listElement) { + const index = mwcList.getIndexOfTarget(this.listElement, evt); + this.mdcListFoundation.handleFocusIn(evt, index); + } + } + + private listOnFocusout(evt: FocusEvent) { + if (this.mdcListFoundation && this.listElement) { + const index = mwcList.getIndexOfTarget(this.listElement, evt); + this.mdcListFoundation.handleFocusOut(evt, index); + } + } + + private listOnKeydown(evt: KeyboardEvent) { + if (this.mdcListFoundation && this.listElement) { + const index = mwcList.getIndexOfTarget(this.listElement, evt); + const target = evt.target as Element; + const elements = mwcList.listElements(this.listElement); + const isRootListItem = elements ? elements.indexOf(target) !== -1 : false; + this.mdcListFoundation.handleKeydown(evt, isRootListItem, index); + } + } + + private listOnClick(evt: MouseEvent) { + if (this.mdcListFoundation && this.listElement) { + const index = mwcList.getIndexOfTarget(this.listElement, evt); + const target = evt.target as Element | null; + const toggleCheckbox = target && 'getAttribute' in target ? + target.getAttribute('role') === 'radio' && + target.getAttribute('aria-checked') === 'true' : + false; + this.mdcListFoundation.handleClick(index, toggleCheckbox); + } + } + + async _getUpdateComplete() { + await super._getUpdateComplete(); + await this._outlineUpdateComplete; + } + async firstUpdated() { const outlineElement = this.outlineElement; if (outlineElement) { - await outlineElement.updateComplete; + this._outlineUpdateComplete = outlineElement.updateComplete; + await this._outlineUpdateComplete; } super.firstUpdated(); @@ -1013,10 +1058,61 @@ export abstract class SelectBase extends FormElement { // if (this.validateOnInitialRender) { // this.reportValidity(); // } + + const menuElement = this.menuElement; + if (!menuElement) { + return; + } + + this.listeners = [ + { + target: menuElement, + name: 'keydown', + cb: this.menuSurfaceOnKeydown.bind(this), + }, + { + target: menuElement, + name: 'opened', + cb: this.menuSurfaceRegisterBodyClick.bind(this), + }, + { + target: menuElement, + name: 'closed', + cb: this.menuSurfaceDeregisterBodyClick.bind(this), + }, + { + target: menuElement, + name: 'opened', + cb: this.menuOnOpened.bind(this), + }, + { + target: menuElement, + name: 'keydown', + cb: this.menuOnKeydown.bind(this), + }, + { + target: menuElement, + name: 'opened', + cb: this.onOpened.bind(this), + }, + { + target: menuElement, + name: 'closed', + cb: this.onClosed.bind(this), + } + ]; + + for (const listener of this.listeners) { + listener.target.addEventListener(listener.name as any, listener.cb); + } } disconnectedCallback() { super.disconnectedCallback(); + + for (const listener of this.listeners) { + listener.target.removeEventListener(listener.name, listener.cb); + } } focus() { From 38957fcd0c376aa2e677bc8ec79e77ba58e12c61 Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Fri, 13 Dec 2019 14:50:47 +1100 Subject: [PATCH 008/161] update deps --- packages/select/package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/select/package.json b/packages/select/package.json index f4ff281075..eed1b7d678 100644 --- a/packages/select/package.json +++ b/packages/select/package.json @@ -10,19 +10,19 @@ "author": "", "license": "Apache-2.0", "dependencies": { - "@material/floating-label": "=5.0.0-canary.aa0eba489.0", - "@material/line-ripple": "=5.0.0-canary.aa0eba489.0", - "@material/dom": "=5.0.0-canary.aa0eba489.0", + "@material/floating-label": "=5.0.0-canary.1c494e567.0", + "@material/line-ripple": "=5.0.0-canary.1c494e567.0", + "@material/dom": "=5.0.0-canary.1c494e567.0", "@material/mwc-base": "^0.11.1", "@material/mwc-floating-label": "^0.11.1", "@material/mwc-icon": "^0.11.1", "@material/mwc-list": "^0.11.1", "@material/mwc-line-ripple": "^0.11.1", "@material/mwc-notched-outline": "^0.11.1", - "@material/select": "=5.0.0-canary.aa0eba489.0", - "@material/menu": "=5.0.0-canary.aa0eba489.0", - "@material/menu-surface": "=5.0.0-canary.aa0eba489.0", - "@material/list": "=5.0.0-canary.aa0eba489.0", + "@material/select": "=5.0.0-canary.1c494e567.0", + "@material/menu": "=5.0.0-canary.1c494e567.0", + "@material/menu-surface": "=5.0.0-canary.1c494e567.0", + "@material/list": "=5.0.0-canary.1c494e567.0", "lit-element": "^2.2.1", "lit-html": "^1.1.2", "tslib": "^1.10.0" From 369d628f5c73045eb8929b8eff5afc29d858fd98 Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Fri, 13 Dec 2019 15:45:55 +1100 Subject: [PATCH 009/161] remove debugger --- packages/select/src/mwc-select-base.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/select/src/mwc-select-base.ts b/packages/select/src/mwc-select-base.ts index d1273b4231..68a78c5fb4 100644 --- a/packages/select/src/mwc-select-base.ts +++ b/packages/select/src/mwc-select-base.ts @@ -816,7 +816,7 @@ export abstract class SelectBase extends FormElement { }, setTransformOrigin: (origin) => { if (this.menuElement) { - mwcMenu.setTransformOrigin(this.menuElement, origin) + mwcMenu.setTransformOrigin(this.menuElement, origin); } }, isFocused: () => { @@ -894,7 +894,6 @@ export abstract class SelectBase extends FormElement { } const anchorElement = mwcMenu.anchorElement(menuElement); - debugger; return anchorElement ? anchorElement.getBoundingClientRect() : null; }, From 92a493760275f187e6548ebc4958f77b2c9ad491 Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Fri, 13 Dec 2019 17:58:16 +1100 Subject: [PATCH 010/161] rename file --- ...urface-directive.ts => mwc-menu-surface-anchor-directive.ts} | 0 packages/select/src/mwc-select-base.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/select/src/{mwc-menu-surface-directive.ts => mwc-menu-surface-anchor-directive.ts} (100%) diff --git a/packages/select/src/mwc-menu-surface-directive.ts b/packages/select/src/mwc-menu-surface-anchor-directive.ts similarity index 100% rename from packages/select/src/mwc-menu-surface-directive.ts rename to packages/select/src/mwc-menu-surface-anchor-directive.ts diff --git a/packages/select/src/mwc-select-base.ts b/packages/select/src/mwc-select-base.ts index 68a78c5fb4..06e372e188 100644 --- a/packages/select/src/mwc-select-base.ts +++ b/packages/select/src/mwc-select-base.ts @@ -37,7 +37,7 @@ import {classMap} from 'lit-html/directives/class-map'; import * as mwcListItem from './mwc-list-item-ponyfill'; import * as mwcList from './mwc-list-ponyfill'; import * as mwcMenu from './mwc-menu-ponyfill'; -import {menuAnchor} from './mwc-menu-surface-directive'; +import {menuAnchor} from './mwc-menu-surface-anchor-directive'; import {isElement} from './util'; // must be done to get past lit-analyzer checks From da40c51d92c44c70da10098f4f15bc8aeced1fa8 Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Tue, 17 Dec 2019 18:46:42 -0800 Subject: [PATCH 011/161] update packages --- packages/list/package.json | 6 +++--- packages/select/package.json | 28 ++++++++++++++-------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/list/package.json b/packages/list/package.json index 7857430e17..ee312ad855 100644 --- a/packages/list/package.json +++ b/packages/list/package.json @@ -1,6 +1,6 @@ { "name": "@material/mwc-list", - "version": "0.11.1", + "version": "0.12.0", "description": "", "main": "mwc-list.js", "module": "mwc-list.js", @@ -10,8 +10,8 @@ "author": "", "license": "Apache-2.0", "dependencies": { - "@material/mwc-base": "^0.11.1", - "@material/list": "=5.0.0-canary.aa0eba489.0", + "@material/mwc-base": "^0.12.0", + "@material/list": "=5.0.0-canary.5ffe8f7e3.0", "lit-element": "^2.2.1", "lit-html": "^1.1.2", "tslib": "^1.10.0" diff --git a/packages/select/package.json b/packages/select/package.json index eed1b7d678..a7d16cc481 100644 --- a/packages/select/package.json +++ b/packages/select/package.json @@ -1,6 +1,6 @@ { "name": "@material/mwc-select", - "version": "0.11.1", + "version": "0.12.0", "description": "", "main": "mwc-select.js", "module": "mwc-select.js", @@ -10,19 +10,19 @@ "author": "", "license": "Apache-2.0", "dependencies": { - "@material/floating-label": "=5.0.0-canary.1c494e567.0", - "@material/line-ripple": "=5.0.0-canary.1c494e567.0", - "@material/dom": "=5.0.0-canary.1c494e567.0", - "@material/mwc-base": "^0.11.1", - "@material/mwc-floating-label": "^0.11.1", - "@material/mwc-icon": "^0.11.1", - "@material/mwc-list": "^0.11.1", - "@material/mwc-line-ripple": "^0.11.1", - "@material/mwc-notched-outline": "^0.11.1", - "@material/select": "=5.0.0-canary.1c494e567.0", - "@material/menu": "=5.0.0-canary.1c494e567.0", - "@material/menu-surface": "=5.0.0-canary.1c494e567.0", - "@material/list": "=5.0.0-canary.1c494e567.0", + "@material/floating-label": "=5.0.0-canary.5ffe8f7e3.0", + "@material/line-ripple": "=5.0.0-canary.5ffe8f7e3.0", + "@material/dom": "=5.0.0-canary.5ffe8f7e3.0", + "@material/mwc-base": "^0.12.0", + "@material/mwc-floating-label": "^0.12.0", + "@material/mwc-icon": "^0.12.0", + "@material/mwc-list": "^0.12.0", + "@material/mwc-line-ripple": "^0.12.0", + "@material/mwc-notched-outline": "^0.12.0", + "@material/select": "=5.0.0-canary.5ffe8f7e3.0", + "@material/menu": "=5.0.0-canary.5ffe8f7e3.0", + "@material/menu-surface": "=5.0.0-canary.5ffe8f7e3.0", + "@material/list": "=5.0.0-canary.5ffe8f7e3.0", "lit-element": "^2.2.1", "lit-html": "^1.1.2", "tslib": "^1.10.0" From 5120eb8bc0bb67c2e66b8fe0f7486e222f591051 Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Tue, 17 Dec 2019 19:01:52 -0800 Subject: [PATCH 012/161] fix anchor --- packages/select/src/mwc-menu-ponyfill.ts | 15 ++++++++++++++- packages/select/src/mwc-select-base.ts | 20 ++------------------ 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/packages/select/src/mwc-menu-ponyfill.ts b/packages/select/src/mwc-menu-ponyfill.ts index 049e9e2772..b1f53a60a4 100644 --- a/packages/select/src/mwc-menu-ponyfill.ts +++ b/packages/select/src/mwc-menu-ponyfill.ts @@ -18,6 +18,7 @@ import {Corner} from '@material/menu-surface/constants'; import MDCMenuSurfaceFoundation from '@material/menu-surface/foundation'; import {getTransformPropertyName} from '@material/menu-surface/util'; import {isElement} from './util'; +import { MDCMenuDistance } from '@material/menu-surface/types'; export const open = (foundation: MDCMenuSurfaceFoundation) => { foundation.open(); @@ -59,9 +60,21 @@ export const setTransformOrigin = (menu: HTMLElement, origin: string) => { }; export const mdcRoot = (menu: Element) => { - return menu.querySelector('.mdc-menu'); + return menu.classList.contains('mdc-menu') ? menu : menu.querySelector('.mdc-menu'); }; +export const position = (menu: HTMLElement, position: Partial) => { + menu.style.left = 'left' in position ? `${position.left}px` : ''; + menu.style.right = 'right' in position ? `${position.right}px` : ''; + menu.style.top = 'top' in position ? `${position.top}px` : ''; + menu.style.bottom = + 'bottom' in position ? `${position.bottom}px` : ''; +} + +export const maxHeight = (menu: HTMLElement, height: string) => { + menu.style.maxHeight = height; +} + export const getDeepFocus = () => { let activeElement = document.activeElement; diff --git a/packages/select/src/mwc-select-base.ts b/packages/select/src/mwc-select-base.ts index 06e372e188..fa6626abee 100644 --- a/packages/select/src/mwc-select-base.ts +++ b/packages/select/src/mwc-select-base.ts @@ -921,17 +921,7 @@ export abstract class SelectBase extends FormElement { return; } - const mdcRoot = mwcMenu.mdcRoot(menuElement) as HTMLElement; - - if (!mdcRoot) { - return; - } - - mdcRoot.style.left = 'left' in position ? `${position.left}px` : ''; - mdcRoot.style.right = 'right' in position ? `${position.right}px` : ''; - mdcRoot.style.top = 'top' in position ? `${position.top}px` : ''; - mdcRoot.style.bottom = - 'bottom' in position ? `${position.bottom}px` : ''; + mwcMenu.position(menuElement, position); }, setMaxHeight: (height) => { const menuElement = this.menuElement; @@ -940,13 +930,7 @@ export abstract class SelectBase extends FormElement { return; } - const mdcRoot = mwcMenu.mdcRoot(menuElement) as HTMLElement; - - if (!mdcRoot) { - return; - } - - mdcRoot.style.maxHeight = height; + mwcMenu.maxHeight(menuElement, height); }, }; From 27fe3b8040d31d1ff30283a7564e204c53e00288 Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Tue, 17 Dec 2019 21:00:26 -0800 Subject: [PATCH 013/161] fix value / floating issues --- packages/select/src/mwc-list-ponyfill.ts | 8 +++++++- packages/select/src/mwc-select-base.ts | 17 +++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/select/src/mwc-list-ponyfill.ts b/packages/select/src/mwc-list-ponyfill.ts index 7c2bfd3f86..fa4c4d8482 100644 --- a/packages/select/src/mwc-list-ponyfill.ts +++ b/packages/select/src/mwc-list-ponyfill.ts @@ -19,7 +19,7 @@ import MDCListFoundation from '@material/list/foundation'; export const selected = (list: Element) => { const children = assignedElements(list); for (const child of children) { - const selected = child.querySelector('.mdc-list-item--selected'); + const selected = child.classList.contains('mdc-list-item--selected') ? child : child.querySelector('.mdc-list-item--selected'); if (selected) { return selected; } @@ -110,6 +110,12 @@ export const wrapFocus = foundation.setWrapFocus(wrapFocus); }; +export const getIndexOfElement = (list: Element, element: Element) => { + const elements = listElements(list); + + return elements.indexOf(element); +} + export const getIndexOfTarget = (list: Element, evt: Event) => { const elements = listElements(list); const path = evt.composedPath(); diff --git a/packages/select/src/mwc-select-base.ts b/packages/select/src/mwc-select-base.ts index fa6626abee..32ff8b50de 100644 --- a/packages/select/src/mwc-select-base.ts +++ b/packages/select/src/mwc-select-base.ts @@ -395,11 +395,12 @@ export abstract class SelectBase extends FormElement { return element.textContent as string; }, - getMenuItemAttr: (menuItem, attr) => { - return menuItem.getAttribute(attr); + getMenuItemAttr: (menuItem) => { + return mwcListItem.value(menuItem); }, addClassAtIndex: (index, className) => { const listElement = this.listElement; + if (!listElement) { return; } @@ -414,6 +415,7 @@ export abstract class SelectBase extends FormElement { }, removeClassAtIndex: (index, className) => { const listElement = this.listElement; + if (!listElement) { return; } @@ -1088,6 +1090,17 @@ export abstract class SelectBase extends FormElement { for (const listener of this.listeners) { listener.target.addEventListener(listener.name as any, listener.cb); } + + if (menuElement) { + const selected = mwcList.selected(menuElement); + + if (selected) { + const index = mwcList.getIndexOfElement(menuElement, selected); + if (index !== -1 && this.mdcFoundation) { + this.mdcFoundation.setSelectedIndex(index); + } + } + } } disconnectedCallback() { From ce988a49cad073ad520df45282b867f14212c6cf Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Tue, 17 Dec 2019 21:00:38 -0800 Subject: [PATCH 014/161] fix notched-outline styles --- packages/select/src/mwc-select.scss | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/select/src/mwc-select.scss b/packages/select/src/mwc-select.scss index 66fa72b425..414a96191b 100644 --- a/packages/select/src/mwc-select.scss +++ b/packages/select/src/mwc-select.scss @@ -22,4 +22,26 @@ limitations under the License. :host { display: inline-block; +} + +.mdc-select--outlined { + &:not(.mdc-select--disabled) { + @include mdc-select-focused-outline-color(primary); + + --mdc-notched-outline-border-color: var(--mdc-select-outlined-idle-border-color, #{$mdc-select-outlined-idle-border}); + + // stylelint-disable-next-line selector-combinator-space-after + &:not(.mdc-select--focused) .mdc-select__selected-text:hover ~ mwc-notched-outline { + --mdc-notched-outline-border-color: var(--mdc-select-outlined-hover-border-color, #{$mdc-select-outlined-hover-border}); + } + + &.mdc-select--focused mwc-notched-outline { + --mdc-notched-outline-stroke-width: 2px; + --mdc-notched-outline-border-color: var(--mdc-select-outlined-focused-border-color, var(--mdc-theme-primary, #{$mdc-theme-primary})); + } + } + + &.mdc-select--disabled.mdc-select--outlined mwc-notched-outline { + --mdc-notched-outline-border-color: var(--mdc-select-outlined-disabled-border-color, #{$mdc-select-outlined-disabled-border}); + } } \ No newline at end of file From 98b5319d7e45d66effacd0941357ad9ff841efa7 Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Tue, 17 Dec 2019 21:01:41 -0800 Subject: [PATCH 015/161] run lint and format --- packages/select/src/mwc-list-item-ponyfill.ts | 108 ++++++++---------- packages/select/src/mwc-list-ponyfill.ts | 12 +- packages/select/src/mwc-menu-ponyfill.ts | 28 ++--- packages/select/src/mwc-select-base.ts | 27 +++-- packages/select/src/util.ts | 2 +- 5 files changed, 89 insertions(+), 88 deletions(-) diff --git a/packages/select/src/mwc-list-item-ponyfill.ts b/packages/select/src/mwc-list-item-ponyfill.ts index 22e256f8d6..c15783744e 100644 --- a/packages/select/src/mwc-list-item-ponyfill.ts +++ b/packages/select/src/mwc-list-item-ponyfill.ts @@ -24,63 +24,55 @@ export const text = (listItem: Element) => { return textContent ? textContent.trim() : ''; }; -export const value = - (listItem: Element) => { - return listItem.getAttribute('data-value') || ''; - } - -export const controlTabIndex = - (listItem: Element, tabIndex: string) => { - const tabbables = listItem.querySelectorAll( - 'button:not(:disabled),a,.radio:not([disabled]),.checkbox:not([disabled]),.tabbable:not([disabled])'); - tabbables.forEach(element => element.setAttribute('tabindex', tabIndex)); - } - -const getCheckbox = - (listItem: Element) => { - return listItem.querySelector( - 'input[type="checkbox"]:not(:disabled),.checkbox:not([disabled])') as - Element | - HasChecked | null; - } - -const getRadio = - (listItem: Element) => { - return listItem.querySelector( - 'input[type="radio"]:not(:disabled),.radio:not([disabled])') as - Element | - HasChecked | null; - } - -export const hasCheckbox = - (listItem: Element) => { - return !!getCheckbox(listItem); - } - -export const hasRadio = - (listItem: Element) => { - return !!getRadio(listItem); - } - -export const isChecked = - (listItem: Element) => { - const checkbox = getCheckbox(listItem); - return checkbox && 'checked' in checkbox ? checkbox.checked : false; - } - -export const setChecked = - (listItem: Element, isChecked: boolean) => { - const checkbox = getCheckbox(listItem); - const radio = getRadio(listItem); - - if (checkbox && 'checked' in checkbox) { - checkbox.checked = isChecked; - } - - if (radio && 'checked' in radio) { - radio.checked = isChecked; - } - } +export const value = (listItem: Element) => { + return listItem.getAttribute('data-value') || ''; +}; + +export const controlTabIndex = (listItem: Element, tabIndex: string) => { + const tabbables = listItem.querySelectorAll( + 'button:not(:disabled),a,.radio:not([disabled]),.checkbox:not([disabled]),.tabbable:not([disabled])'); + tabbables.forEach((element) => element.setAttribute('tabindex', tabIndex)); +}; + +const getCheckbox = (listItem: Element) => { + return listItem.querySelector( + 'input[type="checkbox"]:not(:disabled),.checkbox:not([disabled])') as + Element | + HasChecked | null; +}; + +const getRadio = (listItem: Element) => { + return listItem.querySelector( + 'input[type="radio"]:not(:disabled),.radio:not([disabled])') as + Element | + HasChecked | null; +}; + +export const hasCheckbox = (listItem: Element) => { + return !!getCheckbox(listItem); +}; + +export const hasRadio = (listItem: Element) => { + return !!getRadio(listItem); +}; + +export const isChecked = (listItem: Element) => { + const checkbox = getCheckbox(listItem); + return checkbox && 'checked' in checkbox ? checkbox.checked : false; +}; + +export const setChecked = (listItem: Element, isChecked: boolean) => { + const checkbox = getCheckbox(listItem); + const radio = getRadio(listItem); + + if (checkbox && 'checked' in checkbox) { + checkbox.checked = isChecked; + } + + if (radio && 'checked' in radio) { + radio.checked = isChecked; + } +}; export const hasClass = (listItem: Element, className: string) => - listItem.classList.contains(className); \ No newline at end of file + listItem.classList.contains(className); diff --git a/packages/select/src/mwc-list-ponyfill.ts b/packages/select/src/mwc-list-ponyfill.ts index fa4c4d8482..6ef5a284dc 100644 --- a/packages/select/src/mwc-list-ponyfill.ts +++ b/packages/select/src/mwc-list-ponyfill.ts @@ -19,7 +19,9 @@ import MDCListFoundation from '@material/list/foundation'; export const selected = (list: Element) => { const children = assignedElements(list); for (const child of children) { - const selected = child.classList.contains('mdc-list-item--selected') ? child : child.querySelector('.mdc-list-item--selected'); + const selected = child.classList.contains('mdc-list-item--selected') ? + child : + child.querySelector('.mdc-list-item--selected'); if (selected) { return selected; } @@ -45,7 +47,7 @@ export const assignedElements = (list: Element): Element[] => { if (slot) { return slot.assignedNodes({flatten: true}) - .filter(node => (node.nodeType === Node.ELEMENT_NODE)) as + .filter((node) => (node.nodeType === Node.ELEMENT_NODE)) as Element[]; } @@ -87,7 +89,7 @@ export const getSlottedActiveElement = (list: Element): Element|null => { export const doContentsHaveFocus = (list: Element): boolean => { const activeElement = getSlottedActiveElement(list); if (!activeElement) { - return false + return false; } const elements = assignedElements(list); @@ -114,7 +116,7 @@ export const getIndexOfElement = (list: Element, element: Element) => { const elements = listElements(list); return elements.indexOf(element); -} +}; export const getIndexOfTarget = (list: Element, evt: Event) => { const elements = listElements(list); @@ -128,4 +130,4 @@ export const getIndexOfTarget = (list: Element, evt: Event) => { } return -1; -}; \ No newline at end of file +}; diff --git a/packages/select/src/mwc-menu-ponyfill.ts b/packages/select/src/mwc-menu-ponyfill.ts index b1f53a60a4..7548a94d5b 100644 --- a/packages/select/src/mwc-menu-ponyfill.ts +++ b/packages/select/src/mwc-menu-ponyfill.ts @@ -16,9 +16,10 @@ limitations under the License. */ import {Corner} from '@material/menu-surface/constants'; import MDCMenuSurfaceFoundation from '@material/menu-surface/foundation'; +import {MDCMenuDistance} from '@material/menu-surface/types'; import {getTransformPropertyName} from '@material/menu-surface/util'; + import {isElement} from './util'; -import { MDCMenuDistance } from '@material/menu-surface/types'; export const open = (foundation: MDCMenuSurfaceFoundation) => { foundation.open(); @@ -40,7 +41,7 @@ const assignedElements = (menu: Element): Element[] => { if (slot) { return slot.assignedNodes({flatten: true}) - .filter(node => (isElement(node))) as Element[]; + .filter((node) => (isElement(node))) as Element[]; } return []; @@ -60,20 +61,21 @@ export const setTransformOrigin = (menu: HTMLElement, origin: string) => { }; export const mdcRoot = (menu: Element) => { - return menu.classList.contains('mdc-menu') ? menu : menu.querySelector('.mdc-menu'); + return menu.classList.contains('mdc-menu') ? menu : + menu.querySelector('.mdc-menu'); }; -export const position = (menu: HTMLElement, position: Partial) => { - menu.style.left = 'left' in position ? `${position.left}px` : ''; - menu.style.right = 'right' in position ? `${position.right}px` : ''; - menu.style.top = 'top' in position ? `${position.top}px` : ''; - menu.style.bottom = - 'bottom' in position ? `${position.bottom}px` : ''; -} +export const position = + (menu: HTMLElement, position: Partial) => { + menu.style.left = 'left' in position ? `${position.left}px` : ''; + menu.style.right = 'right' in position ? `${position.right}px` : ''; + menu.style.top = 'top' in position ? `${position.top}px` : ''; + menu.style.bottom = 'bottom' in position ? `${position.bottom}px` : ''; + }; export const maxHeight = (menu: HTMLElement, height: string) => { menu.style.maxHeight = height; -} +}; export const getDeepFocus = () => { let activeElement = document.activeElement; @@ -96,7 +98,7 @@ export const getDeepFocus = () => { export const setPreviousFocus = (menu: Element, previouslyFocused: Element|null) => { (menu as Element & { - _previousFocus: HTMLElement | Element | null + _previousFocus: HTMLElement|Element|null; })._previousFocus = previouslyFocused; }; @@ -113,4 +115,4 @@ export type AnchorableElement = HTMLElement&{anchorElement: Element | null}; export const setAnchorCorner = (foundation: MDCMenuSurfaceFoundation, corner: Corner) => { foundation.setAnchorCorner(corner); - }; \ No newline at end of file + }; diff --git a/packages/select/src/mwc-select-base.ts b/packages/select/src/mwc-select-base.ts index 32ff8b50de..d02312af8e 100644 --- a/packages/select/src/mwc-select-base.ts +++ b/packages/select/src/mwc-select-base.ts @@ -101,7 +101,11 @@ export abstract class SelectBase extends FormElement { @property({type: String}) icon = ''; - protected listeners: ({target: Element, name: string, cb: any})[] = []; + protected listeners: ({ + target: Element; + name: string; + cb: any + })[] = []; protected onBodyClickBound: (evt: MouseEvent) => void = () => {}; protected _outlineUpdateComplete: null|Promise = null; @@ -379,7 +383,7 @@ export abstract class SelectBase extends FormElement { const items = mwcList.listElements(listElement); - return items.map(item => mwcListItem.value(item)); + return items.map((item) => mwcListItem.value(item)); }, getMenuItemTextAtIndex: (index) => { const listElement = this.listElement; @@ -438,7 +442,7 @@ export abstract class SelectBase extends FormElement { if (this.mdcListFoundation) { this.mdcListFoundation.destroy(); - }; + } const mdcListAdapter: MDCListAdapter = { getListItemCount: () => { @@ -614,7 +618,7 @@ export abstract class SelectBase extends FormElement { if (this.mdcMenuFoundation) { this.mdcMenuFoundation.destroy(); - }; + } const mdcMenuAdapter: MDCMenuAdapter = { addClassToElementAtIndex: (index, className) => { @@ -729,14 +733,14 @@ export abstract class SelectBase extends FormElement { } const selectionGroupEl = - closest(elementAtIndex, `.mdc-menu__selection-group`); + closest(elementAtIndex, '.mdc-menu__selection-group'); if (!selectionGroupEl) { return -1; } const selectedItemEl = - selectionGroupEl.querySelector(`.mdc-menu-item--selected`); + selectionGroupEl.querySelector('.mdc-menu-item--selected'); if (!selectedItemEl) { return -1; @@ -773,7 +777,7 @@ export abstract class SelectBase extends FormElement { if (this.mdcMenuSurfaceFoundation) { this.mdcMenuSurfaceFoundation.destroy(); - }; + } const mdcMenuSurfaceAdapter: MDCMenuSurfaceAdapter = { ...addHasRemoveClass(this.menuElement), @@ -901,8 +905,9 @@ export abstract class SelectBase extends FormElement { }, getBodyDimensions: () => { return { - width: document.body.clientWidth, height: document.body.clientHeight, - } + width: document.body.clientWidth, + height: document.body.clientHeight, + }; }, getWindowDimensions: () => { return { @@ -950,7 +955,7 @@ export abstract class SelectBase extends FormElement { protected menuSurfaceOnKeydown(evt: KeyboardEvent) { if (this.mdcMenuSurfaceFoundation) { - this.mdcMenuSurfaceFoundation.handleKeydown(evt) + this.mdcMenuSurfaceFoundation.handleKeydown(evt); } } @@ -1156,7 +1161,7 @@ export abstract class SelectBase extends FormElement { } const normalizedX = xCoord - targetClientRect.left; - this.mdcFoundation.handleClick(normalizedX) + this.mdcFoundation.handleClick(normalizedX); } } diff --git a/packages/select/src/util.ts b/packages/select/src/util.ts index 33a4fd331a..5223d41a30 100644 --- a/packages/select/src/util.ts +++ b/packages/select/src/util.ts @@ -1,3 +1,3 @@ export const isElement = (node: Node) => { return node.nodeType === Node.ELEMENT_NODE; -}; \ No newline at end of file +}; From b2624f028dd75320acd5819bd148c9fe4e9e3cc4 Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Tue, 17 Dec 2019 21:04:13 -0800 Subject: [PATCH 016/161] fix lint --- packages/select/src/mwc-list-ponyfill.ts | 38 ++++++++++++------------ packages/select/src/mwc-select-base.ts | 4 +-- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/select/src/mwc-list-ponyfill.ts b/packages/select/src/mwc-list-ponyfill.ts index 6ef5a284dc..d7fe3f0105 100644 --- a/packages/select/src/mwc-list-ponyfill.ts +++ b/packages/select/src/mwc-list-ponyfill.ts @@ -16,6 +16,18 @@ */ import MDCListFoundation from '@material/list/foundation'; +export const assignedElements = (list: Element): Element[] => { + const slot = list.querySelector('slot'); + + if (slot) { + return slot.assignedNodes({flatten: true}) + .filter((node) => (node.nodeType === Node.ELEMENT_NODE)) as + Element[]; + } + + return []; +}; + export const selected = (list: Element) => { const children = assignedElements(list); for (const child of children) { @@ -42,18 +54,6 @@ export const select = (list: Element, itemToSelect: Element) => { itemToSelect.removeAttribute('aria-selected'); }; -export const assignedElements = (list: Element): Element[] => { - const slot = list.querySelector('slot'); - - if (slot) { - return slot.assignedNodes({flatten: true}) - .filter((node) => (node.nodeType === Node.ELEMENT_NODE)) as - Element[]; - } - - return []; -}; - export const listElements = (list: Element): Element[] => { const nodes = assignedElements(list); const listItems = @@ -76,6 +76,13 @@ export const mdcRoot = (list: Element) => { return list as HTMLElement; }; +export const getElementAtIndex = + (list: Element, index: number): Element|undefined => { + const elements = listElements(list); + + return elements[index] as Element | undefined; + }; + export const getSlottedActiveElement = (list: Element): Element|null => { const first = getElementAtIndex(list, 0); if (!first) { @@ -100,13 +107,6 @@ export const doContentsHaveFocus = (list: Element): boolean => { }, false); }; -export const getElementAtIndex = - (list: Element, index: number): Element|undefined => { - const elements = listElements(list); - - return elements[index] as Element | undefined; - }; - export const wrapFocus = (foundation: MDCListFoundation, wrapFocus: boolean) => { foundation.setWrapFocus(wrapFocus); diff --git a/packages/select/src/mwc-select-base.ts b/packages/select/src/mwc-select-base.ts index d02312af8e..6061285d66 100644 --- a/packages/select/src/mwc-select-base.ts +++ b/packages/select/src/mwc-select-base.ts @@ -104,7 +104,7 @@ export abstract class SelectBase extends FormElement { protected listeners: ({ target: Element; name: string; - cb: any + cb: any; })[] = []; protected onBodyClickBound: (evt: MouseEvent) => void = () => {}; protected _outlineUpdateComplete: null|Promise = null; @@ -311,7 +311,7 @@ export abstract class SelectBase extends FormElement { } }, getAnchorElement: () => this.anchorElement, - setMenuAnchorElement: () => { /* Handled by anchor directive */ }, + setMenuAnchorElement: () => {/* Handled by anchor directive */}, setMenuAnchorCorner: (anchorCorner) => { if (this.mdcMenuSurfaceFoundation) { mwcMenu.setAnchorCorner(this.mdcMenuSurfaceFoundation, anchorCorner); From 8947e6d4b01e8052fd2eea418a78dfc3864a699d Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Tue, 17 Dec 2019 21:05:05 -0800 Subject: [PATCH 017/161] fix lint format loop --- packages/select/src/mwc-select-base.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/select/src/mwc-select-base.ts b/packages/select/src/mwc-select-base.ts index 6061285d66..3ee471d6ce 100644 --- a/packages/select/src/mwc-select-base.ts +++ b/packages/select/src/mwc-select-base.ts @@ -311,7 +311,9 @@ export abstract class SelectBase extends FormElement { } }, getAnchorElement: () => this.anchorElement, - setMenuAnchorElement: () => {/* Handled by anchor directive */}, + setMenuAnchorElement: () => { + /* Handled by anchor directive */ + }, setMenuAnchorCorner: (anchorCorner) => { if (this.mdcMenuSurfaceFoundation) { mwcMenu.setAnchorCorner(this.mdcMenuSurfaceFoundation, anchorCorner); From 278ae43a30fa2d7cd02ab36d3ce88671af58d74d Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Mon, 6 Jan 2020 16:57:42 -0800 Subject: [PATCH 018/161] fix resize and alignment issue --- packages/select/src/mwc-select.scss | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/select/src/mwc-select.scss b/packages/select/src/mwc-select.scss index 414a96191b..551421affa 100644 --- a/packages/select/src/mwc-select.scss +++ b/packages/select/src/mwc-select.scss @@ -22,6 +22,16 @@ limitations under the License. :host { display: inline-block; + vertical-align: top; +} + +.mdc-select .mdc-select__anchor { + display: block; + overflow: hidden; + + * { + display: inline-flex; + } } .mdc-select--outlined { From 81889eef1137f30067efedf58a0b34c608453ecc Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Mon, 6 Jan 2020 17:48:48 -0800 Subject: [PATCH 019/161] fixed sizing overflow --- packages/select/src/mwc-select.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/select/src/mwc-select.scss b/packages/select/src/mwc-select.scss index 551421affa..e0ad5d49c0 100644 --- a/packages/select/src/mwc-select.scss +++ b/packages/select/src/mwc-select.scss @@ -27,7 +27,10 @@ limitations under the License. .mdc-select .mdc-select__anchor { display: block; - overflow: hidden; + + .mdc-select__selected-text { + overflow: hidden; + } * { display: inline-flex; From 8ff4f78b923d4812cd1a3fef737f5ce432cab3a8 Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Mon, 6 Jan 2020 17:48:57 -0800 Subject: [PATCH 020/161] fix tabbing --- packages/select/src/mwc-list-item-ponyfill.ts | 18 ++++++++++++++++-- packages/select/src/mwc-list-ponyfill.ts | 12 ++++++++++++ packages/select/src/mwc-select-base.ts | 14 ++++++++++++-- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/packages/select/src/mwc-list-item-ponyfill.ts b/packages/select/src/mwc-list-item-ponyfill.ts index c15783744e..7d8796f1b4 100644 --- a/packages/select/src/mwc-list-item-ponyfill.ts +++ b/packages/select/src/mwc-list-item-ponyfill.ts @@ -14,6 +14,13 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + +const focusableChildSelector = ` + button:not(:disabled), + a, + .radio:not([disabled]), + .checkbox:not([disabled]), + .tabbable:not([disabled])`; interface HasChecked extends Element { checked: boolean; } @@ -29,8 +36,7 @@ export const value = (listItem: Element) => { }; export const controlTabIndex = (listItem: Element, tabIndex: string) => { - const tabbables = listItem.querySelectorAll( - 'button:not(:disabled),a,.radio:not([disabled]),.checkbox:not([disabled]),.tabbable:not([disabled])'); + const tabbables = listItem.querySelectorAll(focusableChildSelector); tabbables.forEach((element) => element.setAttribute('tabindex', tabIndex)); }; @@ -76,3 +82,11 @@ export const setChecked = (listItem: Element, isChecked: boolean) => { export const hasClass = (listItem: Element, className: string) => listItem.classList.contains(className); + +export const init = (listItem: Element) => { + if (!listItem.hasAttribute('tabindex')) { + listItem.setAttribute('tabindex', '-1'); + } + + controlTabIndex(listItem, '-1'); +}; diff --git a/packages/select/src/mwc-list-ponyfill.ts b/packages/select/src/mwc-list-ponyfill.ts index d7fe3f0105..c912a18162 100644 --- a/packages/select/src/mwc-list-ponyfill.ts +++ b/packages/select/src/mwc-list-ponyfill.ts @@ -16,6 +16,8 @@ */ import MDCListFoundation from '@material/list/foundation'; +import * as mwcListItem from './mwc-list-item-ponyfill'; + export const assignedElements = (list: Element): Element[] => { const slot = list.querySelector('slot'); @@ -131,3 +133,13 @@ export const getIndexOfTarget = (list: Element, evt: Event) => { return -1; }; + +export const layout = (list: Element, foundation: MDCListFoundation) => { + const elements = listElements(list); + + for (const element of elements) { + mwcListItem.init(element); + } + + foundation.layout(); +}; diff --git a/packages/select/src/mwc-select-base.ts b/packages/select/src/mwc-select-base.ts index 3ee471d6ce..67f0158396 100644 --- a/packages/select/src/mwc-select-base.ts +++ b/packages/select/src/mwc-select-base.ts @@ -25,7 +25,7 @@ import {MDCMenuSurfaceAdapter} from '@material/menu-surface/adapter'; import MDCMenuSurfaceFoundation from '@material/menu-surface/foundation.js'; import {MDCMenuAdapter} from '@material/menu/adapter'; import MDCMenuFoundation from '@material/menu/foundation.js'; -import {addHasRemoveClass, FormElement} from '@material/mwc-base/form-element.js'; +import {addHasRemoveClass, FormElement, observer} from '@material/mwc-base/form-element.js'; import {floatingLabel, FloatingLabel} from '@material/mwc-floating-label'; import {lineRipple, LineRipple} from '@material/mwc-line-ripple'; import {NotchedOutline} from '@material/mwc-notched-outline'; @@ -85,6 +85,11 @@ export abstract class SelectBase extends FormElement { @query('.mdc-select__anchor') protected anchorElement!: HTMLDivElement|null; @property({type: Boolean, attribute: 'disabled', reflect: true}) + @observer(function(this: SelectBase, value: boolean) { + if (this.mdcFoundation) { + this.mdcFoundation.setDisabled(value); + } + }) disabled = false; @property({type: Boolean}) outlined = false; @@ -134,7 +139,6 @@ export abstract class SelectBase extends FormElement {
      Date: Tue, 7 Jan 2020 19:14:38 -0800 Subject: [PATCH 021/161] fix slotted menu item styles and focus --- packages/select/src/mwc-list-ponyfill.ts | 1 + packages/select/src/mwc-select.scss | 363 +++++++++++++++++++++++ 2 files changed, 364 insertions(+) diff --git a/packages/select/src/mwc-list-ponyfill.ts b/packages/select/src/mwc-list-ponyfill.ts index c912a18162..90cec4fcfb 100644 --- a/packages/select/src/mwc-list-ponyfill.ts +++ b/packages/select/src/mwc-list-ponyfill.ts @@ -141,5 +141,6 @@ export const layout = (list: Element, foundation: MDCListFoundation) => { mwcListItem.init(element); } + foundation.setUseActivatedClass(true); foundation.layout(); }; diff --git a/packages/select/src/mwc-select.scss b/packages/select/src/mwc-select.scss index e0ad5d49c0..a7af6e8903 100644 --- a/packages/select/src/mwc-select.scss +++ b/packages/select/src/mwc-select.scss @@ -23,6 +23,7 @@ limitations under the License. :host { display: inline-block; vertical-align: top; + outline: none; } .mdc-select .mdc-select__anchor { @@ -57,4 +58,366 @@ limitations under the License. &.mdc-select--disabled.mdc-select--outlined mwc-notched-outline { --mdc-notched-outline-border-color: var(--mdc-select-outlined-disabled-border-color, #{$mdc-select-outlined-disabled-border}); } +} + +// listitem +$query: mdc-feature-all(); +$feat-structure: mdc-feature-create-target($query, structure); +$feat-color: mdc-feature-create-target($query, color); +$feat-animation: mdc-feature-create-target($query, animation); +$item-primary-text-baseline-height: 32px; +$item-secondary-text-baseline-height: 20px; +$dense-item-primary-text-baseline-height: 24px; + +.mdc-list ::slotted(.mdc-list-item) { + @include mdc-list-item-base_; + + outline: none; + + $height: mdc-density-prop-value( + $density-config: $mdc-list-single-line-density-config, + $density-scale: $mdc-list-single-line-density-scale, + $property-name: height, + ); + + @include mdc-list-single-line-height($height, $query: $query); +} + +// "Selected" is ephemeral and likely to change soon. E.g., selecting one or more photos to share in Google Photos. +// "Activated" is more permanent. E.g., the currently highlighted navigation destination in a drawer. +::slotted(.mdc-list-item--selected), +::slotted(.mdc-list-item--activated) { + @include mdc-list-item-primary-text-ink-color(primary, $query); + @include mdc-list-item-graphic-ink-color(primary, $query); +} + +::slotted(.mdc-list-item__graphic) { + @include mdc-feature-targets($feat-structure) { + @include mdc-list-graphic-size_(24px); + + flex-shrink: 0; + align-items: center; + justify-content: center; + fill: currentColor; + } +} + +// Extra specificity is to override .material-icons display style if used in +// conjunction with mdc-list-item__graphic +// stylelint-disable plugin/selector-bem-pattern +.mdc-list ::slotted(.mdc-list-item__graphic) { + @include mdc-feature-targets($feat-structure) { + display: inline-flex; + } +} +// stylelint-enable plugin/selector-bem-pattern + +::slotted(.mdc-list-item__meta) { + // stylelint-disable selector-class-pattern + &:not(.material-icons) { + @include mdc-typography(caption, $query); + } + // stylelint-enable selector-class-pattern + + @include mdc-feature-targets($feat-structure) { + @include mdc-rtl-reflexive-property(margin, auto, 0, "::slotted(.mdc-list-item)"); + } +} + +::slotted(.mdc-list-item__text) { + @include mdc-typography-overflow-ellipsis($query); +} + +// Disable interaction on label elements that may automatically +// toggle corresponding checkbox / radio input. +::slotted(.mdc-list-item__text)[for] { + @include mdc-feature-targets($feat-structure) { + pointer-events: none; + } +} + +::slotted(.mdc-list-item__primary-text) { + @include mdc-typography-overflow-ellipsis($query); + @include mdc-typography-baseline-top($item-primary-text-baseline-height, $query); + @include mdc-typography-baseline-bottom($item-secondary-text-baseline-height, $query); + + @include mdc-feature-targets($feat-structure) { + display: block; + } + + // stylelint-disable plugin/selector-bem-pattern + .mdc-list--dense & { + @include mdc-typography-baseline-top($dense-item-primary-text-baseline-height, $query); + @include mdc-typography-baseline-bottom($item-secondary-text-baseline-height, $query); + } + // stylelint-enable plugin/selector-bem-pattern +} + +::slotted(.mdc-list-item__secondary-text) { + @include mdc-typography(body2, $query); + @include mdc-typography-overflow-ellipsis($query); + @include mdc-typography-baseline-top($item-secondary-text-baseline-height, $query); + + @include mdc-feature-targets($feat-structure) { + display: block; + } + + // stylelint-disable plugin/selector-bem-pattern + .mdc-list--dense & { + @include mdc-typography-baseline-top($item-secondary-text-baseline-height, $query); + + @include mdc-feature-targets($feat-structure) { + font-size: inherit; + } + } + // stylelint-enable plugin/selector-bem-pattern +} + +// stylelint-disable plugin/selector-bem-pattern +.mdc-list--dense ::slotted(.mdc-list-item) { + @include mdc-feature-targets($feat-structure) { + height: 40px; + } +} + +.mdc-list--dense ::slotted(.mdc-list-item__graphic) { + @include mdc-feature-targets($feat-structure) { + @include mdc-list-graphic-size_(20px); + } +} + +.mdc-list--avatar-list ::slotted(.mdc-list-item) { + @include mdc-feature-targets($feat-structure) { + height: 56px; + } +} + +.mdc-list--avatar-list ::slotted(.mdc-list-item__graphic) { + @include mdc-feature-targets($feat-structure) { + @include mdc-list-graphic-size_(40px); + + border-radius: 50%; + } +} + +.mdc-list--two-line ::slotted(.mdc-list-item__text) { + @include mdc-feature-targets($feat-structure) { + align-self: flex-start; + } +} + +.mdc-list--two-line ::slotted(.mdc-list-item) { + @include mdc-feature-targets($feat-structure) { + height: 72px; + } +} + +.mdc-list--two-line.mdc-list--dense ::slotted(.mdc-list-item), +.mdc-list--avatar-list.mdc-list--dense ::slotted(.mdc-list-item) { + @include mdc-feature-targets($feat-structure) { + height: 60px; + } +} + +.mdc-list--avatar-list.mdc-list--dense ::slotted(.mdc-list-item__graphic) { + @include mdc-feature-targets($feat-structure) { + @include mdc-list-graphic-size_(36px); + } +} + +// Only change mouse cursor for interactive list items which are not disabled. +:not(.mdc-list--non-interactive) > :not(::slotted(.mdc-list-item--disabled))::slotted(.mdc-list-item) { + @include mdc-feature-targets($feat-structure) { + cursor: pointer; + } +} + +// Override anchor tag styles for the use-case of a list being used for navigation +// stylelint-disable selector-max-type,selector-no-qualifying-type +::slotted(a.mdc-list-item) { + @include mdc-feature-targets($feat-structure) { + color: inherit; + text-decoration: none; + } +} + +.mdc-list:not(.mdc-list--non-interactive) { + $states-color: mdc-theme-prop-value(on-surface); + $hover-opacity: mdc-states-opacity($states-color, hover); + $focus-opacity: mdc-states-opacity($states-color, focus); + $press-opacity: mdc-states-opacity($states-color, press); + + ::slotted(.mdc-list-item:not(.mdc-list-item--disabled)) { + @include mdc-ripple-surface($query); + @include mdc-ripple-radius-bounded($query: $query); + @include mdc-ripple-target-selector("&") { + @include mdc-states-base-color($states-color, $query); + } + } + + // Hover + + ::slotted(.mdc-list-item:not(.mdc-list-item--disabled):hover)::before { + // Opacity falls under color because the chosen opacity is color-dependent + // in typical usage + @include mdc-feature-targets($feat-color) { + opacity: $hover-opacity; + } + } + + // Focus + + ::slotted(.mdc-list-item:not(.mdc-list-item--disabled).mdc-ripple-upgraded--background-focused), + ::slotted(.mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded):focus) { + &::before { + @include mdc-states-focus-opacity-properties_( + $opacity: $focus-opacity, $query: $query); + } + } + + // Press + + ::slotted(.mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded)) { + // Apply press additively by using the ::after pseudo-element + &::after { + @include mdc-feature-targets($feat-animation) { + transition: opacity $mdc-ripple-fade-out-duration linear; + } + } + } + + ::slotted(.mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded):active) { + &::after { + @include mdc-feature-targets($feat-animation) { + transition-duration: $mdc-ripple-fade-in-duration; + } + + // Opacity falls under color because the chosen opacity is color-dependent in typical usage + @include mdc-feature-targets($feat-color) { + opacity: $press-opacity; + } + } + } + + ::slotted(.mdc-list-item:not(.mdc-list-item--disabled).mdc-ripple-upgraded) { + @include mdc-feature-targets($feat-color) { + --mdc-ripple-fg-opacity: #{$press-opacity}; + } + } + + // selected and active + + $selected-opacity: mdc-states-opacity(primary, selected); + $activated-opacity: mdc-states-opacity(primary, activated); + $states-color-primary: primary; + $hover-opacity-primary: mdc-states-opacity($states-color-primary, hover); + $focus-opacity-primary: mdc-states-opacity($states-color-primary, focus); + $press-opacity-primary: mdc-states-opacity($states-color-primary, press); + + // selected + + ::slotted(.mdc-list-item--selected:not(.mdc-list-item--disabled)) { + &::before { + // Opacity falls under color because the chosen opacity is color-dependent. + @include mdc-feature-targets($feat-color) { + opacity: $selected-opacity; + } + } + + @include mdc-ripple-target-selector("&") { + @include mdc-states-base-color(primary, $query); + } + } + + // Hover-selected + + ::slotted(.mdc-list-item--selected.mdc-list-item:not(.mdc-list-item--disabled):hover)::before { + // Opacity falls under color because the chosen opacity is color-dependent + // in typical usage + @include mdc-feature-targets($feat-color) { + opacity: $hover-opacity-primary + $selected-opacity; + } + } + + // Focus-selected + + ::slotted(.mdc-list-item--selected.mdc-list-item:not(.mdc-list-item--disabled).mdc-ripple-upgraded--background-focused), + ::slotted(.mdc-list-item--selected.mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded):focus) { + &::before { + @include mdc-states-focus-opacity-properties_( + $opacity: $focus-opacity-primary + $selected-opacity, $query: $query); + } + } + + // Press-selected + ::slotted(.mdc-list-item--selected.mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded):active) { + &::after { + // Opacity falls under color because the chosen opacity is color-dependent in typical usage + @include mdc-feature-targets($feat-color) { + opacity: $press-opacity-primary + $selected-opacity; + } + } + } + + ::slotted(.mdc-list-item--selected.mdc-list-item:not(.mdc-list-item--disabled).mdc-ripple-upgraded) { + @include mdc-feature-targets($feat-color) { + --mdc-ripple-fg-opacity: #{$press-opacity-primary + $selected-opacity}; + } + } + + // Activated + + ::slotted(.mdc-list-item--activated:not(.mdc-list-item--disabled)) { + &::before { + @include mdc-feature-targets($feat-color) { + opacity: $activated-opacity; + } + } + + @include mdc-ripple-target-selector("&") { + @include mdc-states-base-color(primary, $query); + } + } + + // Hover-activated + + ::slotted(.mdc-list-item--activated.mdc-list-item:not(.mdc-list-item--disabled):hover)::before { + // Opacity falls under color because the chosen opacity is color-dependent + // in typical usage + @include mdc-feature-targets($feat-color) { + opacity: $hover-opacity-primary + $activated-opacity; + } + } + + // Focus-activated + + ::slotted(.mdc-list-item--activated.mdc-list-item:not(.mdc-list-item--disabled).mdc-ripple-upgraded--background-focused), + ::slotted(.mdc-list-item--activated.mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded):focus) { + &::before { + @include mdc-states-focus-opacity-properties_( + $opacity: $focus-opacity-primary + $activated-opacity, $query: $query); + } + } + + // Press-activated + ::slotted(.mdc-list-item--activated.mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded):active) { + &::after { + // Opacity falls under color because the chosen opacity is color-dependent in typical usage + @include mdc-feature-targets($feat-color) { + opacity: $press-opacity-primary + $activated-opacity; + } + } + } + + ::slotted(.mdc-list-item--activated.mdc-list-item:not(.mdc-list-item--disabled).mdc-ripple-upgraded) { + @include mdc-feature-targets($feat-color) { + --mdc-ripple-fg-opacity: #{$press-opacity-primary + $activated-opacity}; + } + } +} + +// menu + +.mdc-menu { + width: 100%; } \ No newline at end of file From 70da6b2e1950662be448eb2412d57d60c0582b74 Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Wed, 8 Jan 2020 14:02:27 -0800 Subject: [PATCH 022/161] fix lit linting --- packages/select/src/mwc-select-base.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/select/src/mwc-select-base.ts b/packages/select/src/mwc-select-base.ts index 67f0158396..fcf4b11cbe 100644 --- a/packages/select/src/mwc-select-base.ts +++ b/packages/select/src/mwc-select-base.ts @@ -109,7 +109,7 @@ export abstract class SelectBase extends FormElement { protected listeners: ({ target: Element; name: string; - cb: any; + cb: EventListenerOrEventListenerObject; })[] = []; protected onBodyClickBound: (evt: MouseEvent) => void = () => {}; protected _outlineUpdateComplete: null|Promise = null; @@ -134,6 +134,7 @@ export abstract class SelectBase extends FormElement {
      ${this.icon ? this.renderIcon(this.icon) : ''} +
      @@ -1070,7 +1071,8 @@ export abstract class SelectBase extends FormElement { { target: menuElement, name: 'keydown', - cb: this.menuSurfaceOnKeydown.bind(this), + cb: this.menuSurfaceOnKeydown.bind(this) as + EventListenerOrEventListenerObject, }, { target: menuElement, @@ -1090,7 +1092,7 @@ export abstract class SelectBase extends FormElement { { target: menuElement, name: 'keydown', - cb: this.menuOnKeydown.bind(this), + cb: this.menuOnKeydown.bind(this) as EventListenerOrEventListenerObject, }, { target: menuElement, @@ -1105,7 +1107,8 @@ export abstract class SelectBase extends FormElement { ]; for (const listener of this.listeners) { - listener.target.addEventListener(listener.name as any, listener.cb); + listener.target.addEventListener( + listener.name, listener.cb as EventListenerOrEventListenerObject); } if (menuElement) { From 4b12226098245d7aafa7c6c1654102a0948dd61d Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Wed, 8 Jan 2020 14:02:49 -0800 Subject: [PATCH 023/161] fix cursor on list --- packages/select/src/mwc-select.scss | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/select/src/mwc-select.scss b/packages/select/src/mwc-select.scss index a7af6e8903..c0c99a8f06 100644 --- a/packages/select/src/mwc-select.scss +++ b/packages/select/src/mwc-select.scss @@ -247,7 +247,13 @@ $dense-item-primary-text-baseline-height: 24px; $focus-opacity: mdc-states-opacity($states-color, focus); $press-opacity: mdc-states-opacity($states-color, press); + ::slotted(.mdc-list-item) { + cursor: pointer; + user-select: none; + } + ::slotted(.mdc-list-item:not(.mdc-list-item--disabled)) { + @include mdc-ripple-surface($query); @include mdc-ripple-radius-bounded($query: $query); @include mdc-ripple-target-selector("&") { @@ -255,6 +261,10 @@ $dense-item-primary-text-baseline-height: 24px; } } + ::slotted(.mdc-list-item.mdc-list-item--disabled) { + cursor: default; + } + // Hover ::slotted(.mdc-list-item:not(.mdc-list-item--disabled):hover)::before { From 190ecdd78a80b6b1edd5d179668b148b134bb651 Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Wed, 8 Jan 2020 17:34:13 -0800 Subject: [PATCH 024/161] mwc-list and mwc-list-item --- packages/base/src/utils.ts | 11 +- packages/list/src/mwc-list-base.ts | 337 +++++++++++++++++++++++ packages/list/src/mwc-list-item-base.ts | 96 ++++++- packages/list/src/mwc-list.scss | 18 ++ packages/list/src/mwc-list.ts | 31 +++ packages/select/src/mwc-list-ponyfill.ts | 14 +- packages/select/src/mwc-menu-ponyfill.ts | 5 +- packages/select/src/mwc-select-base.ts | 72 ++--- packages/select/src/util.ts | 3 - 9 files changed, 525 insertions(+), 62 deletions(-) create mode 100644 packages/list/src/mwc-list-base.ts create mode 100644 packages/list/src/mwc-list.scss create mode 100644 packages/list/src/mwc-list.ts delete mode 100644 packages/select/src/util.ts diff --git a/packages/base/src/utils.ts b/packages/base/src/utils.ts index b192f57599..278d07a9c6 100644 --- a/packages/base/src/utils.ts +++ b/packages/base/src/utils.ts @@ -21,9 +21,18 @@ limitations under the License. import {matches} from '@material/dom/ponyfill'; +/** + * Determines whether a node is an element. + * + * @param node Node to check + */ +export const isNodeElement = (node: Node) => { + return node.nodeType === Node.ELEMENT_NODE; +}; + export function findAssignedElement(slot: HTMLSlotElement, selector: string) { for (const node of slot.assignedNodes({flatten: true})) { - if (node.nodeType === Node.ELEMENT_NODE) { + if (isNodeElement(node)) { const el = (node as HTMLElement); if (matches(el, selector)) { return el; diff --git a/packages/list/src/mwc-list-base.ts b/packages/list/src/mwc-list-base.ts new file mode 100644 index 0000000000..2ef2c46192 --- /dev/null +++ b/packages/list/src/mwc-list-base.ts @@ -0,0 +1,337 @@ +/** +@license +Copyright 2020 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import '@material/mwc-notched-outline'; + +import {BaseElement} from '@material/mwc-base/base-element.js'; +import {MDCListAdapter} from '@material/list/adapter'; +import MDCListFoundation from '@material/list/foundation.js'; +import {html, query} from 'lit-element'; +import {isNodeElement} from '@material/mwc-base/utils'; +import {ListItemBase} from './mwc-list-item-base'; + +export abstract class ListBase extends BaseElement { + protected mdcFoundation!: MDCListFoundation; + + protected readonly mdcFoundationClass = MDCListFoundation; + + @query('.mdc-list') protected mdcRoot!: HTMLElement; + + @query('slot') protected slotElement!: HTMLSlotElement|null; + + protected get assignedElements(): Element[] { + const slot = this.slotElement; + + if (slot) { + return slot.assignedNodes({flatten: true}) + .filter((node) => (node.nodeType === Node.ELEMENT_NODE)) as + Element[]; + } + + return []; + } + + get items(): ListItemBase[] { + const nodes = this.assignedElements; + const listItems = + nodes + .map((element) => { + if (element instanceof ListItemBase) { + return element; + } + + return Array.from(element.querySelectorAll('.list-item')); + }) + .reduce((listItems, listItemResult) => { + return listItems.concat(listItemResult); + }, []); + + return listItems as ListItemBase[]; + } + + render() { + return html` +
        + +
      + `; + } + + createAdapter(): MDCListAdapter { + return { + getListItemCount: () => { + if (this.mdcRoot) { + const elements = this.items; + return elements.length; + } + + return 0; + }, + getFocusedElementIndex: () => { + if (!this.mdcRoot) { + return -1; + } + + const elements = this.items; + + if (!elements.length) { + return -1; + } + + const activeElement = this.getSlottedActiveElement(); + + if (!activeElement) { + return -1; + } + + let activeItem: ListItemBase|Element|null = activeElement; + + while (activeItem && !(activeItem instanceof ListItemBase)) { + const parent = activeItem.parentNode; + if (!parent) { + activeItem = null; + continue; + } + + if (parent.nodeType === Node.ELEMENT_NODE) { + activeItem = parent as Element; + } else { + activeItem = null; + } + } + + const activeListItem = activeItem as ListItemBase | null; + + return activeListItem ? elements.indexOf(activeListItem) : -1; + }, + getAttributeForElementIndex: (index, attr) => { + const listElement = this.mdcRoot; + if (!listElement) { + return ''; + } + + const element = this.items[index]; + return element ? element.getAttribute(attr) : ''; + }, + setAttributeForElementIndex: (index, attr, val) => { + if (!this.mdcRoot) { + return; + } + + const element = this.items[index]; + + if (element) { + element.setAttribute(attr, val); + } + }, + addClassForElementIndex: (index, className) => { + if (!this.mdcRoot) { + return; + } + + const element = this.items[index]; + if (element) { + element.classList.add(className); + } + }, + removeClassForElementIndex: (index, className) => { + if (!this.mdcRoot) { + return; + } + + const element = this.items[index]; + if (element) { + element.classList.remove(className); + } + }, + focusItemAtIndex: (index) => { + if (!this.mdcRoot) { + return; + } + + const element = this.items[index]; + if (element && isNodeElement(element)) { + (element as HTMLElement).focus(); + } + }, + setTabIndexForListItemChildren: (index, tabIndex) => { + if (!this.mdcRoot) { + return; + } + + const element = this.items[index]; + if (element) { + element.setControlTabIndex(tabIndex); + } + }, + hasCheckboxAtIndex: (index) => { + if (!this.mdcRoot) { + return false; + } + + const element = this.items[index]; + return element ? element.hasCheckbox : false; + }, + hasRadioAtIndex: (index) => { + if (!this.mdcRoot) { + return false; + } + + const element = this.items[index]; + return element ? element.hasRadio : false; + }, + isCheckboxCheckedAtIndex: (index) => { + if (!this.mdcRoot) { + return false; + } + + const element = this.items[index]; + return element ? element.hasCheckbox && element.isControlChecked() : false; + }, + setCheckedCheckboxOrRadioAtIndex: (index, isChecked) => { + if (!this.mdcRoot) { + return; + } + + const element = this.items[index]; + if (element) { + element.setControlChecked(isChecked); + } + }, + notifyAction: (index) => { + if (!this.mdcRoot) { + return; + } + + const init: CustomEventInit = {bubbles: true}; + init.detail = {index}; + const ev = new CustomEvent('action', init); + this.mdcRoot.dispatchEvent(ev); + }, + isFocusInsideList: () => { + if (!this.mdcRoot) { + return false; + } + + return this.doContentsHaveFocus(); + }, + isRootFocused: () => { + if (!this.mdcRoot) { + return false; + } + + const mdcRoot = this.mdcRoot; + const root = mdcRoot.getRootNode() as unknown as DocumentOrShadowRoot; + return root.activeElement === mdcRoot; + }, + listItemAtIndexHasClass: (index, className) => { + if (!this.mdcRoot) { + return false; + } + + const item = this.items[index]; + + if (!item) { + return false; + } + + return item.classList.contains(className); + }, + }; + } + + protected getSlottedActiveElement(): Element|null { + const first = this.items[0]; + if (!first) { + return null; + } + + const root = first.getRootNode() as unknown as DocumentOrShadowRoot; + return root ? root.activeElement : null; + } + + protected doContentsHaveFocus(): boolean { + const activeElement = this.getSlottedActiveElement(); + if (!activeElement) { + return false; + } + + const elements = this.assignedElements; + + return elements.reduce((isContained: boolean, listItem) => { + return isContained || listItem === activeElement || + listItem.contains(activeElement); + }, false); + } + + protected listOnFocusin(evt: FocusEvent) { + if (this.mdcFoundation && this.mdcRoot) { + const index = this.getIndexOfTarget(evt); + this.mdcFoundation.handleFocusIn(evt, index); + } + } + + protected listOnFocusout(evt: FocusEvent) { + if (this.mdcFoundation && this.mdcRoot) { + const index = this.getIndexOfTarget(evt); + this.mdcFoundation.handleFocusOut(evt, index); + } + } + + protected listOnKeydown(evt: KeyboardEvent) { + if (this.mdcFoundation && this.mdcRoot) { + const index = this.getIndexOfTarget(evt); + const target = evt.target as Element; + const isRootListItem = target instanceof ListItemBase; + this.mdcFoundation.handleKeydown(evt, isRootListItem, index); + } + } + + protected listOnClick(evt: MouseEvent) { + if (this.mdcFoundation && this.mdcRoot) { + const index = this.getIndexOfTarget(evt); + const target = evt.target as Element | null; + const toggleCheckbox = target && 'getAttribute' in target ? + target.getAttribute('role') === 'radio' && + target.getAttribute('aria-checked') === 'true' : + false; + this.mdcFoundation.handleClick(index, toggleCheckbox); + } + } + + protected getIndexOfTarget(evt: Event): number { + const elements = this.items; + const path = evt.composedPath(); + + for (const pathItem of path) { + let index = -1; + if (pathItem instanceof ListItemBase) { + index = elements.indexOf(pathItem); + } + + if (index !== -1) { + return index; + } + } + + return -1; + } +} diff --git a/packages/list/src/mwc-list-item-base.ts b/packages/list/src/mwc-list-item-base.ts index 8af1fee3aa..d4a43b75b0 100644 --- a/packages/list/src/mwc-list-item-base.ts +++ b/packages/list/src/mwc-list-item-base.ts @@ -1,16 +1,88 @@ /** -@license -Copyright 2019 Google Inc. All Rights Reserved. + @license + Copyright 2019 Google Inc. All Rights Reserved. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import {LitElement, query, html, property} from 'lit-element'; + +interface HasChecked extends Element { + checked: boolean; +} +export class ListItemBase extends LitElement { + @query('slot') protected slotElement!: HTMLSlotElement|null; + @query('label') protected labelElement!: HTMLLabelElement|null; + + @property({type: String}) value = ''; + @property({type: Boolean}) hasCheckbox = false; + @property({type: Boolean}) hasRadio = false; + @property({type: Boolean, reflect: true, attribute: 'disabled'}) disabled = false; + + get text() { + const textContent = this.textContent; + + return textContent ? textContent.trim() : ''; + } + + protected createRenderRoot() { + return this.attachShadow({mode: 'open', delegatesFocus: true}); + } + + render() { + return html` + `; + } + + protected getControl(): HasChecked | null { + const label = this.labelElement; + + if (!label) { + return null; + } + + const control = label.control as HTMLElement | HasChecked | null; + + return control && 'checked' in control ? control : null; + } + + setControlTabIndex(tabIndex: string) { + const control = this.getControl(); + if (control) { + control.setAttribute('tabindex', tabIndex); + } + } + + isControlChecked() { + const control = this.getControl(); + return control ? control.checked : false; + } + + setControlChecked(isChecked: boolean) { + const control = this.getControl(); + + if (control) { + control.checked = isChecked; + } + } + + firstUpdated() { + if (!this.hasAttribute('tabindex')) { + this.setAttribute('tabindex', '-1'); + } + + this.setControlTabIndex('-1'); + } +} diff --git a/packages/list/src/mwc-list.scss b/packages/list/src/mwc-list.scss new file mode 100644 index 0000000000..38df18a384 --- /dev/null +++ b/packages/list/src/mwc-list.scss @@ -0,0 +1,18 @@ +/** +@license +Copyright 2019 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +@import "@material/list/mdc-list.scss"; \ No newline at end of file diff --git a/packages/list/src/mwc-list.ts b/packages/list/src/mwc-list.ts new file mode 100644 index 0000000000..d368b7fb60 --- /dev/null +++ b/packages/list/src/mwc-list.ts @@ -0,0 +1,31 @@ +/** +@license +Copyright 2020 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {customElement} from 'lit-element'; +import {ListBase} from './mwc-list-base.js'; +import {style} from './mwc-list-css.js'; + +declare global { + interface HTMLElementTagNameMap { + 'mwc-list': List; + } +} + +@customElement('mwc-list') +export class List extends ListBase { + static styles = style; +} diff --git a/packages/select/src/mwc-list-ponyfill.ts b/packages/select/src/mwc-list-ponyfill.ts index 90cec4fcfb..16111cc2b7 100644 --- a/packages/select/src/mwc-list-ponyfill.ts +++ b/packages/select/src/mwc-list-ponyfill.ts @@ -56,7 +56,7 @@ export const select = (list: Element, itemToSelect: Element) => { itemToSelect.removeAttribute('aria-selected'); }; -export const listElements = (list: Element): Element[] => { +export const items = (list: Element): Element[] => { const nodes = assignedElements(list); const listItems = nodes @@ -78,15 +78,15 @@ export const mdcRoot = (list: Element) => { return list as HTMLElement; }; -export const getElementAtIndex = +export const getItemAtIndex = (list: Element, index: number): Element|undefined => { - const elements = listElements(list); + const elements = items(list); return elements[index] as Element | undefined; }; export const getSlottedActiveElement = (list: Element): Element|null => { - const first = getElementAtIndex(list, 0); + const first = getItemAtIndex(list, 0); if (!first) { return null; } @@ -115,13 +115,13 @@ export const wrapFocus = }; export const getIndexOfElement = (list: Element, element: Element) => { - const elements = listElements(list); + const elements = items(list); return elements.indexOf(element); }; export const getIndexOfTarget = (list: Element, evt: Event) => { - const elements = listElements(list); + const elements = items(list); const path = evt.composedPath(); for (const pathItem of path) { @@ -135,7 +135,7 @@ export const getIndexOfTarget = (list: Element, evt: Event) => { }; export const layout = (list: Element, foundation: MDCListFoundation) => { - const elements = listElements(list); + const elements = items(list); for (const element of elements) { mwcListItem.init(element); diff --git a/packages/select/src/mwc-menu-ponyfill.ts b/packages/select/src/mwc-menu-ponyfill.ts index 7548a94d5b..2b13aafe8b 100644 --- a/packages/select/src/mwc-menu-ponyfill.ts +++ b/packages/select/src/mwc-menu-ponyfill.ts @@ -15,12 +15,11 @@ See the License for the specific language governing permissions and limitations under the License. */ import {Corner} from '@material/menu-surface/constants'; +import {isNodeElement} from '@material/mwc-base/utils'; import MDCMenuSurfaceFoundation from '@material/menu-surface/foundation'; import {MDCMenuDistance} from '@material/menu-surface/types'; import {getTransformPropertyName} from '@material/menu-surface/util'; -import {isElement} from './util'; - export const open = (foundation: MDCMenuSurfaceFoundation) => { foundation.open(); }; @@ -41,7 +40,7 @@ const assignedElements = (menu: Element): Element[] => { if (slot) { return slot.assignedNodes({flatten: true}) - .filter((node) => (isElement(node))) as Element[]; + .filter((node) => (isNodeElement(node))) as Element[]; } return []; diff --git a/packages/select/src/mwc-select-base.ts b/packages/select/src/mwc-select-base.ts index fcf4b11cbe..15570f5959 100644 --- a/packages/select/src/mwc-select-base.ts +++ b/packages/select/src/mwc-select-base.ts @@ -38,7 +38,7 @@ import * as mwcListItem from './mwc-list-item-ponyfill'; import * as mwcList from './mwc-list-ponyfill'; import * as mwcMenu from './mwc-menu-ponyfill'; import {menuAnchor} from './mwc-menu-surface-anchor-directive'; -import {isElement} from './util'; +import {isNodeElement} from '@material/mwc-base/utils'; // must be done to get past lit-analyzer checks declare global { @@ -335,7 +335,7 @@ export abstract class SelectBase extends FormElement { return; } - const element = mwcList.getElementAtIndex(listElement, index); + const element = mwcList.getItemAtIndex(listElement, index); if (!element) { return; @@ -349,7 +349,7 @@ export abstract class SelectBase extends FormElement { return; } - const element = mwcList.getElementAtIndex(listElement, index); + const element = mwcList.getItemAtIndex(listElement, index); if (!element) { return; @@ -363,7 +363,7 @@ export abstract class SelectBase extends FormElement { return; } - const element = mwcList.getElementAtIndex(listElement, index); + const element = mwcList.getItemAtIndex(listElement, index); if (!element) { return; @@ -375,7 +375,7 @@ export abstract class SelectBase extends FormElement { const listElement = this.listElement; if (listElement) { - const elements = mwcList.listElements(listElement); + const elements = mwcList.items(listElement); return elements.length; } @@ -388,7 +388,7 @@ export abstract class SelectBase extends FormElement { return []; } - const items = mwcList.listElements(listElement); + const items = mwcList.items(listElement); return items.map((item) => mwcListItem.value(item)); }, @@ -398,7 +398,7 @@ export abstract class SelectBase extends FormElement { return ''; } - const element = mwcList.getElementAtIndex(listElement, index); + const element = mwcList.getItemAtIndex(listElement, index); if (!element) { return ''; @@ -416,7 +416,7 @@ export abstract class SelectBase extends FormElement { return; } - const element = mwcList.getElementAtIndex(listElement, index); + const element = mwcList.getItemAtIndex(listElement, index); if (!element) { return; @@ -431,7 +431,7 @@ export abstract class SelectBase extends FormElement { return; } - const element = mwcList.getElementAtIndex(listElement, index); + const element = mwcList.getItemAtIndex(listElement, index); if (!element) { return; @@ -454,7 +454,7 @@ export abstract class SelectBase extends FormElement { const mdcListAdapter: MDCListAdapter = { getListItemCount: () => { if (this.listElement) { - const elements = mwcList.listElements(this.listElement); + const elements = mwcList.items(this.listElement); return elements.length; } @@ -465,7 +465,7 @@ export abstract class SelectBase extends FormElement { return -1; } - const elements = mwcList.listElements(this.listElement); + const elements = mwcList.items(this.listElement); if (!elements.length) { return -1; @@ -485,7 +485,7 @@ export abstract class SelectBase extends FormElement { return ''; } - const element = mwcList.getElementAtIndex(listElement, index); + const element = mwcList.getItemAtIndex(listElement, index); return element ? element.getAttribute(attr) : ''; }, setAttributeForElementIndex: (index, attr, val) => { @@ -493,7 +493,7 @@ export abstract class SelectBase extends FormElement { return; } - const element = mwcList.getElementAtIndex(this.listElement, index); + const element = mwcList.getItemAtIndex(this.listElement, index); if (element) { element.setAttribute(attr, val); @@ -504,7 +504,7 @@ export abstract class SelectBase extends FormElement { return; } - const element = mwcList.getElementAtIndex(this.listElement, index); + const element = mwcList.getItemAtIndex(this.listElement, index); if (element) { element.classList.add(className); } @@ -514,7 +514,7 @@ export abstract class SelectBase extends FormElement { return; } - const element = mwcList.getElementAtIndex(this.listElement, index); + const element = mwcList.getItemAtIndex(this.listElement, index); if (element) { element.classList.remove(className); } @@ -524,8 +524,8 @@ export abstract class SelectBase extends FormElement { return; } - const element = mwcList.getElementAtIndex(this.listElement, index); - if (element && isElement(element)) { + const element = mwcList.getItemAtIndex(this.listElement, index); + if (element && isNodeElement(element)) { (element as HTMLElement).focus(); } }, @@ -534,7 +534,7 @@ export abstract class SelectBase extends FormElement { return; } - const element = mwcList.getElementAtIndex(this.listElement, index); + const element = mwcList.getItemAtIndex(this.listElement, index); if (element) { mwcListItem.controlTabIndex(element, tabIndex); } @@ -544,7 +544,7 @@ export abstract class SelectBase extends FormElement { return false; } - const element = mwcList.getElementAtIndex(this.listElement, index); + const element = mwcList.getItemAtIndex(this.listElement, index); return element ? mwcListItem.hasCheckbox(element) : false; }, hasRadioAtIndex: (index) => { @@ -552,7 +552,7 @@ export abstract class SelectBase extends FormElement { return false; } - const element = mwcList.getElementAtIndex(this.listElement, index); + const element = mwcList.getItemAtIndex(this.listElement, index); return element ? mwcListItem.hasRadio(element) : false; }, isCheckboxCheckedAtIndex: (index) => { @@ -560,7 +560,7 @@ export abstract class SelectBase extends FormElement { return false; } - const element = mwcList.getElementAtIndex(this.listElement, index); + const element = mwcList.getItemAtIndex(this.listElement, index); return element ? mwcListItem.hasRadio(element) : false; }, setCheckedCheckboxOrRadioAtIndex: (index, isChecked) => { @@ -568,7 +568,7 @@ export abstract class SelectBase extends FormElement { return; } - const element = mwcList.getElementAtIndex(this.listElement, index); + const element = mwcList.getItemAtIndex(this.listElement, index); if (element) { mwcListItem.setChecked(element, isChecked); } @@ -604,7 +604,7 @@ export abstract class SelectBase extends FormElement { return false; } - const item = mwcList.getElementAtIndex(this.listElement, index); + const item = mwcList.getItemAtIndex(this.listElement, index); if (!item) { return false; @@ -633,7 +633,7 @@ export abstract class SelectBase extends FormElement { return; } - const element = mwcList.getElementAtIndex(this.listElement, index); + const element = mwcList.getItemAtIndex(this.listElement, index); if (!element) { return; @@ -646,7 +646,7 @@ export abstract class SelectBase extends FormElement { return; } - const element = mwcList.getElementAtIndex(this.listElement, index); + const element = mwcList.getItemAtIndex(this.listElement, index); if (!element) { return; @@ -659,7 +659,7 @@ export abstract class SelectBase extends FormElement { return; } - const element = mwcList.getElementAtIndex(this.listElement, index); + const element = mwcList.getItemAtIndex(this.listElement, index); if (!element) { return; @@ -672,7 +672,7 @@ export abstract class SelectBase extends FormElement { return; } - const element = mwcList.getElementAtIndex(this.listElement, index); + const element = mwcList.getItemAtIndex(this.listElement, index); if (!element) { return; @@ -689,7 +689,7 @@ export abstract class SelectBase extends FormElement { }, getElementIndex: (element) => { if (this.listElement) { - return mwcList.listElements(this.listElement).indexOf(element); + return mwcList.items(this.listElement).indexOf(element); } return -1; @@ -709,16 +709,16 @@ export abstract class SelectBase extends FormElement { return 0; } - return mwcList.listElements(this.listElement).length; + return mwcList.items(this.listElement).length; }, focusItemAtIndex: (index) => { if (!this.listElement) { return; } - const element = mwcList.getElementAtIndex(this.listElement, index); + const element = mwcList.getItemAtIndex(this.listElement, index); - if (element && isElement(element)) { + if (element && isNodeElement(element)) { (element as HTMLElement).focus(); } }, @@ -733,7 +733,7 @@ export abstract class SelectBase extends FormElement { } const elementAtIndex = - mwcList.getElementAtIndex(this.listElement, index); + mwcList.getItemAtIndex(this.listElement, index); if (!elementAtIndex) { return -1; @@ -753,7 +753,7 @@ export abstract class SelectBase extends FormElement { return -1; } - const elements = mwcList.listElements(this.listElement); + const elements = mwcList.items(this.listElement); return elements.indexOf(selectedItemEl); }, @@ -763,7 +763,7 @@ export abstract class SelectBase extends FormElement { } const elementAtIndex = - mwcList.getElementAtIndex(this.listElement, index); + mwcList.getItemAtIndex(this.listElement, index); if (!elementAtIndex) { return false; @@ -989,7 +989,7 @@ export abstract class SelectBase extends FormElement { protected menuOnAction(evt: CustomEvent<{index: number}>) { if (this.mdcMenuFoundation && this.listElement) { - const el = mwcList.getElementAtIndex(this.listElement, evt.detail.index); + const el = mwcList.getItemAtIndex(this.listElement, evt.detail.index); if (el) { this.mdcMenuFoundation.handleItemAction(el); } @@ -1020,7 +1020,7 @@ export abstract class SelectBase extends FormElement { if (this.mdcListFoundation && this.listElement) { const index = mwcList.getIndexOfTarget(this.listElement, evt); const target = evt.target as Element; - const elements = mwcList.listElements(this.listElement); + const elements = mwcList.items(this.listElement); const isRootListItem = elements ? elements.indexOf(target) !== -1 : false; this.mdcListFoundation.handleKeydown(evt, isRootListItem, index); } diff --git a/packages/select/src/util.ts b/packages/select/src/util.ts deleted file mode 100644 index 5223d41a30..0000000000 --- a/packages/select/src/util.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const isElement = (node: Node) => { - return node.nodeType === Node.ELEMENT_NODE; -}; From 51e177dd8fbeaa8afeb4d9f09add23abb12c8dc9 Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Wed, 8 Jan 2020 20:01:55 -0800 Subject: [PATCH 025/161] initial pull out of mwc-list and item --- packages/base/src/utils.ts | 24 ++ packages/list/src/mwc-list-base.ts | 166 +++++--- packages/list/src/mwc-list-item-base.ts | 28 +- packages/list/src/mwc-list-item.scss | 7 +- packages/list/src/mwc-list-item.ts | 18 +- packages/list/src/mwc-list.scss | 368 ++++++++++++++++- packages/select/src/mwc-list-item-ponyfill.ts | 92 ----- packages/select/src/mwc-list-ponyfill.ts | 146 ------- packages/select/src/mwc-menu-ponyfill.ts | 2 +- packages/select/src/mwc-select-base.ts | 378 ++++-------------- packages/select/src/mwc-select.scss | 367 ----------------- 11 files changed, 634 insertions(+), 962 deletions(-) delete mode 100644 packages/select/src/mwc-list-item-ponyfill.ts delete mode 100644 packages/select/src/mwc-list-ponyfill.ts diff --git a/packages/base/src/utils.ts b/packages/base/src/utils.ts index 278d07a9c6..a6f53a3d63 100644 --- a/packages/base/src/utils.ts +++ b/packages/base/src/utils.ts @@ -72,3 +72,27 @@ document.removeEventListener('x', fn); * Do event listeners suport the `passive` option? */ export const supportsPassiveEventListener = supportsPassive; + +export const slotActiveElement = (slot: HTMLSlotElement): Element|null => { + const assignedElements = + slot.assignedNodes({flatten: true}) + .filter((node) => isNodeElement(node)) as Element[]; + + const first = assignedElements[0]; + if (!first) { + return null; + } + + const root = first.getRootNode() as unknown as DocumentOrShadowRoot; + return root ? root.activeElement : null; +}; + +export const doesSlotContainElement = + (slot: HTMLSlotElement, element: Element) => { + return slot.assignedNodes({flatten: true}) + .filter((node) => isNodeElement(node)) + .reduce((isContained: boolean, assinedElement) => { + return isContained || assinedElement === element || + assinedElement.contains(element); + }, false); + }; diff --git a/packages/list/src/mwc-list-base.ts b/packages/list/src/mwc-list-base.ts index 2ef2c46192..b362a2a267 100644 --- a/packages/list/src/mwc-list-base.ts +++ b/packages/list/src/mwc-list-base.ts @@ -16,11 +16,13 @@ limitations under the License. */ import '@material/mwc-notched-outline'; -import {BaseElement} from '@material/mwc-base/base-element.js'; import {MDCListAdapter} from '@material/list/adapter'; import MDCListFoundation from '@material/list/foundation.js'; -import {html, query} from 'lit-element'; -import {isNodeElement} from '@material/mwc-base/utils'; +import {MDCListIndex} from '@material/list/types'; +import {BaseElement, observer} from '@material/mwc-base/base-element.js'; +import {isNodeElement, doesSlotContainElement} from '@material/mwc-base/utils'; +import {html, property, query} from 'lit-element'; + import {ListItemBase} from './mwc-list-item-base'; export abstract class ListBase extends BaseElement { @@ -32,12 +34,20 @@ export abstract class ListBase extends BaseElement { @query('slot') protected slotElement!: HTMLSlotElement|null; + @property({type: Boolean}) + @observer(function(this: ListBase, value: boolean) { + if (this.mdcFoundation) { + this.mdcFoundation.setSingleSelection(!value); + } + }) + multi = false; + protected get assignedElements(): Element[] { const slot = this.slotElement; if (slot) { return slot.assignedNodes({flatten: true}) - .filter((node) => (node.nodeType === Node.ELEMENT_NODE)) as + .filter((node) => isNodeElement(node)) as Element[]; } @@ -62,6 +72,20 @@ export abstract class ListBase extends BaseElement { return listItems as ListItemBase[]; } + protected selected_: ListItemBase|null = null; + + get selected(): ListItemBase|null { + return this.selected_; + } + + get index(): MDCListIndex { + if (this.mdcFoundation) { + return this.mdcFoundation.getSelectedIndex(); + } + + return -1; + } + render() { return html`
        { @@ -150,6 +227,11 @@ export abstract class ListBase extends BaseElement { const element = this.items[index]; if (element) { element.classList.add(className); + + if (className === 'mdc-list-item--selected' || + className === 'mdc-list-item--activated') { + this.select(index); + } } }, removeClassForElementIndex: (index, className) => { @@ -204,7 +286,8 @@ export abstract class ListBase extends BaseElement { } const element = this.items[index]; - return element ? element.hasCheckbox && element.isControlChecked() : false; + return element ? element.hasCheckbox && element.isControlChecked() : + false; }, setCheckedCheckboxOrRadioAtIndex: (index, isChecked) => { if (!this.mdcRoot) { @@ -224,7 +307,7 @@ export abstract class ListBase extends BaseElement { const init: CustomEventInit = {bubbles: true}; init.detail = {index}; const ev = new CustomEvent('action', init); - this.mdcRoot.dispatchEvent(ev); + this.dispatchEvent(ev); }, isFocusInsideList: () => { if (!this.mdcRoot) { @@ -268,70 +351,43 @@ export abstract class ListBase extends BaseElement { return root ? root.activeElement : null; } - protected doContentsHaveFocus(): boolean { + doContentsHaveFocus(): boolean { + const slotElement = this.slotElement; const activeElement = this.getSlottedActiveElement(); - if (!activeElement) { + if (!activeElement || !slotElement) { return false; } - const elements = this.assignedElements; - - return elements.reduce((isContained: boolean, listItem) => { - return isContained || listItem === activeElement || - listItem.contains(activeElement); - }, false); + return doesSlotContainElement(slotElement, activeElement); } - protected listOnFocusin(evt: FocusEvent) { - if (this.mdcFoundation && this.mdcRoot) { - const index = this.getIndexOfTarget(evt); - this.mdcFoundation.handleFocusIn(evt, index); - } - } + select(index: number) { + const previouslySelected = this.selected; + const itemToSelect = this.items[index]; - protected listOnFocusout(evt: FocusEvent) { - if (this.mdcFoundation && this.mdcRoot) { - const index = this.getIndexOfTarget(evt); - this.mdcFoundation.handleFocusOut(evt, index); + if (!itemToSelect) { + return; } - } - protected listOnKeydown(evt: KeyboardEvent) { - if (this.mdcFoundation && this.mdcRoot) { - const index = this.getIndexOfTarget(evt); - const target = evt.target as Element; - const isRootListItem = target instanceof ListItemBase; - this.mdcFoundation.handleKeydown(evt, isRootListItem, index); + if (previouslySelected) { + previouslySelected.selected = false; } + + + itemToSelect.selected = true; + this.selected_ = itemToSelect; } - protected listOnClick(evt: MouseEvent) { - if (this.mdcFoundation && this.mdcRoot) { - const index = this.getIndexOfTarget(evt); - const target = evt.target as Element | null; - const toggleCheckbox = target && 'getAttribute' in target ? - target.getAttribute('role') === 'radio' && - target.getAttribute('aria-checked') === 'true' : - false; - this.mdcFoundation.handleClick(index, toggleCheckbox); + wrapFocus(wrapFocus: boolean) { + if (this.mdcFoundation) { + this.mdcFoundation.setWrapFocus(wrapFocus); } } - protected getIndexOfTarget(evt: Event): number { - const elements = this.items; - const path = evt.composedPath(); - - for (const pathItem of path) { - let index = -1; - if (pathItem instanceof ListItemBase) { - index = elements.indexOf(pathItem); - } + firstUpdated() { + super.firstUpdated(); - if (index !== -1) { - return index; - } - } - - return -1; + this.mdcFoundation.layout(); + this.mdcFoundation.setSingleSelection(!this.multi); } } diff --git a/packages/list/src/mwc-list-item-base.ts b/packages/list/src/mwc-list-item-base.ts index d4a43b75b0..bf0f7faec0 100644 --- a/packages/list/src/mwc-list-item-base.ts +++ b/packages/list/src/mwc-list-item-base.ts @@ -15,7 +15,8 @@ limitations under the License. */ -import {LitElement, query, html, property} from 'lit-element'; +import {observer} from '@material/mwc-base/observer'; +import {html, LitElement, property, query} from 'lit-element'; interface HasChecked extends Element { checked: boolean; @@ -27,7 +28,18 @@ export class ListItemBase extends LitElement { @property({type: String}) value = ''; @property({type: Boolean}) hasCheckbox = false; @property({type: Boolean}) hasRadio = false; - @property({type: Boolean, reflect: true, attribute: 'disabled'}) disabled = false; + @property({type: Boolean, reflect: true, attribute: 'disabled'}) + disabled = false; + + @property({type: Boolean, reflect: true, attribute: 'selected'}) + @observer(function(this: ListItemBase, value: boolean) { + if (value) { + this.setAttribute('aria-selected', 'true'); + } else { + this.setAttribute('aria-selected', 'false'); + } + }) + selected = false; get text() { const textContent = this.textContent; @@ -41,12 +53,14 @@ export class ListItemBase extends LitElement { render() { return html` - `; +
      • + +
      • `; } - protected getControl(): HasChecked | null { + protected getControl(): HasChecked|null { const label = this.labelElement; if (!label) { @@ -79,6 +93,8 @@ export class ListItemBase extends LitElement { } firstUpdated() { + this.setAttribute('role', 'option'); + if (!this.hasAttribute('tabindex')) { this.setAttribute('tabindex', '-1'); } diff --git a/packages/list/src/mwc-list-item.scss b/packages/list/src/mwc-list-item.scss index 38df18a384..ab9f543474 100644 --- a/packages/list/src/mwc-list-item.scss +++ b/packages/list/src/mwc-list-item.scss @@ -15,4 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -@import "@material/list/mdc-list.scss"; \ No newline at end of file +@import "@material/list/mdc-list.scss"; + +:host { + display: list-item; + outline: none; +} \ No newline at end of file diff --git a/packages/list/src/mwc-list-item.ts b/packages/list/src/mwc-list-item.ts index 8af1fee3aa..c1f6926713 100644 --- a/packages/list/src/mwc-list-item.ts +++ b/packages/list/src/mwc-list-item.ts @@ -1,6 +1,6 @@ /** @license -Copyright 2019 Google Inc. All Rights Reserved. +Copyright 2020 Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,3 +14,19 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + +import {customElement} from 'lit-element'; + +import {style} from './mwc-list-css.js'; +import {ListItemBase} from './mwc-list-item-base.js'; + +declare global { + interface HTMLElementTagNameMap { + 'mwc-list-item': ListItem; + } +} + +@customElement('mwc-list-item') +export class ListItem extends ListItemBase { + static styles = style; +} diff --git a/packages/list/src/mwc-list.scss b/packages/list/src/mwc-list.scss index 38df18a384..b65167b75a 100644 --- a/packages/list/src/mwc-list.scss +++ b/packages/list/src/mwc-list.scss @@ -15,4 +15,370 @@ See the License for the specific language governing permissions and limitations under the License. */ -@import "@material/list/mdc-list.scss"; \ No newline at end of file +@import "@material/list/mdc-list.scss"; + +// listitem +$query: mdc-feature-all(); +$feat-structure: mdc-feature-create-target($query, structure); +$feat-color: mdc-feature-create-target($query, color); +$feat-animation: mdc-feature-create-target($query, animation); +$item-primary-text-baseline-height: 32px; +$item-secondary-text-baseline-height: 20px; +$dense-item-primary-text-baseline-height: 24px; + +.mdc-list ::slotted(mwc-list-item,.list-item) { + @include mdc-list-item-base_; + + outline: none; + + $height: mdc-density-prop-value( + $density-config: $mdc-list-single-line-density-config, + $density-scale: $mdc-list-single-line-density-scale, + $property-name: height, + ); + + @include mdc-list-single-line-height($height, $query: $query); +} + +// "Selected" is ephemeral and likely to change soon. E.g., selecting one or more photos to share in Google Photos. +// "Activated" is more permanent. E.g., the currently highlighted navigation destination in a drawer. +::slotted([selected]), +::slotted(.mdc-list-item--activated) { + @include mdc-list-item-primary-text-ink-color(primary, $query); + @include mdc-list-item-graphic-ink-color(primary, $query); +} + +::slotted(.mdc-list-item__graphic) { + @include mdc-feature-targets($feat-structure) { + @include mdc-list-graphic-size_(24px); + + flex-shrink: 0; + align-items: center; + justify-content: center; + fill: currentColor; + } +} + +// Extra specificity is to override .material-icons display style if used in +// conjunction with mdc-list-item__graphic +// stylelint-disable plugin/selector-bem-pattern +.mdc-list ::slotted(.mdc-list-item__graphic) { + @include mdc-feature-targets($feat-structure) { + display: inline-flex; + } +} +// stylelint-enable plugin/selector-bem-pattern + +::slotted(.mdc-list-item__meta) { + // stylelint-disable selector-class-pattern + &:not(.material-icons) { + @include mdc-typography(caption, $query); + } + // stylelint-enable selector-class-pattern + + @include mdc-feature-targets($feat-structure) { + @include mdc-rtl-reflexive-property(margin, auto, 0, "::slotted(mwc-list-item,.list-item)"); + } +} + +::slotted(.mdc-list-item__text) { + @include mdc-typography-overflow-ellipsis($query); +} + +// Disable interaction on label elements that may automatically +// toggle corresponding checkbox / radio input. +::slotted(.mdc-list-item__text)[for] { + @include mdc-feature-targets($feat-structure) { + pointer-events: none; + } +} + +::slotted(.mdc-list-item__primary-text) { + @include mdc-typography-overflow-ellipsis($query); + @include mdc-typography-baseline-top($item-primary-text-baseline-height, $query); + @include mdc-typography-baseline-bottom($item-secondary-text-baseline-height, $query); + + @include mdc-feature-targets($feat-structure) { + display: block; + } + + // stylelint-disable plugin/selector-bem-pattern + .mdc-list--dense & { + @include mdc-typography-baseline-top($dense-item-primary-text-baseline-height, $query); + @include mdc-typography-baseline-bottom($item-secondary-text-baseline-height, $query); + } + // stylelint-enable plugin/selector-bem-pattern +} + +::slotted(.mdc-list-item__secondary-text) { + @include mdc-typography(body2, $query); + @include mdc-typography-overflow-ellipsis($query); + @include mdc-typography-baseline-top($item-secondary-text-baseline-height, $query); + + @include mdc-feature-targets($feat-structure) { + display: block; + } + + // stylelint-disable plugin/selector-bem-pattern + .mdc-list--dense & { + @include mdc-typography-baseline-top($item-secondary-text-baseline-height, $query); + + @include mdc-feature-targets($feat-structure) { + font-size: inherit; + } + } + // stylelint-enable plugin/selector-bem-pattern +} + +// stylelint-disable plugin/selector-bem-pattern +.mdc-list--dense ::slotted(mwc-list-item,.list-item) { + @include mdc-feature-targets($feat-structure) { + height: 40px; + } +} + +.mdc-list--dense ::slotted(.mdc-list-item__graphic) { + @include mdc-feature-targets($feat-structure) { + @include mdc-list-graphic-size_(20px); + } +} + +.mdc-list--avatar-list ::slotted(mwc-list-item,.list-item) { + @include mdc-feature-targets($feat-structure) { + height: 56px; + } +} + +.mdc-list--avatar-list ::slotted(.mdc-list-item__graphic) { + @include mdc-feature-targets($feat-structure) { + @include mdc-list-graphic-size_(40px); + + border-radius: 50%; + } +} + +.mdc-list--two-line ::slotted(.mdc-list-item__text) { + @include mdc-feature-targets($feat-structure) { + align-self: flex-start; + } +} + +.mdc-list--two-line ::slotted(mwc-list-item,.list-item) { + @include mdc-feature-targets($feat-structure) { + height: 72px; + } +} + +.mdc-list--two-line.mdc-list--dense ::slotted(mwc-list-item,.list-item), +.mdc-list--avatar-list.mdc-list--dense ::slotted(mwc-list-item,.list-item) { + @include mdc-feature-targets($feat-structure) { + height: 60px; + } +} + +.mdc-list--avatar-list.mdc-list--dense ::slotted(.mdc-list-item__graphic) { + @include mdc-feature-targets($feat-structure) { + @include mdc-list-graphic-size_(36px); + } +} + +// Only change mouse cursor for interactive list items which are not disabled. +:not(.mdc-list--non-interactive) > :not(::slotted(.mdc-list-item--disabled))::slotted(mwc-list-item,.list-item) { + @include mdc-feature-targets($feat-structure) { + cursor: pointer; + } +} + +// Override anchor tag styles for the use-case of a list being used for navigation +// stylelint-disable selector-max-type,selector-no-qualifying-type +::slotted(amwc-list-item,.list-item) { + @include mdc-feature-targets($feat-structure) { + color: inherit; + text-decoration: none; + } +} + +.mdc-list:not(.mdc-list--non-interactive) { + $states-color: mdc-theme-prop-value(on-surface); + $hover-opacity: mdc-states-opacity($states-color, hover); + $focus-opacity: mdc-states-opacity($states-color, focus); + $press-opacity: mdc-states-opacity($states-color, press); + + ::slotted(mwc-list-item,.list-item) { + cursor: pointer; + user-select: none; + } + + ::slotted(.mdc-list-item:not(.mdc-list-item--disabled)) { + + @include mdc-ripple-surface($query); + @include mdc-ripple-radius-bounded($query: $query); + @include mdc-ripple-target-selector("&") { + @include mdc-states-base-color($states-color, $query); + } + } + + ::slotted(.mdc-list-item.mdc-list-item--disabled) { + cursor: default; + } + + // Hover + + ::slotted(.mdc-list-item:not(.mdc-list-item--disabled):hover)::before { + // Opacity falls under color because the chosen opacity is color-dependent + // in typical usage + @include mdc-feature-targets($feat-color) { + opacity: $hover-opacity; + } + } + + // Focus + + ::slotted(.mdc-list-item:not(.mdc-list-item--disabled).mdc-ripple-upgraded--background-focused), + ::slotted(.mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded):focus) { + &::before { + @include mdc-states-focus-opacity-properties_( + $opacity: $focus-opacity, $query: $query); + } + } + + // Press + + ::slotted(.mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded)) { + // Apply press additively by using the ::after pseudo-element + &::after { + @include mdc-feature-targets($feat-animation) { + transition: opacity $mdc-ripple-fade-out-duration linear; + } + } + } + + ::slotted(.mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded):active) { + &::after { + @include mdc-feature-targets($feat-animation) { + transition-duration: $mdc-ripple-fade-in-duration; + } + + // Opacity falls under color because the chosen opacity is color-dependent in typical usage + @include mdc-feature-targets($feat-color) { + opacity: $press-opacity; + } + } + } + + ::slotted(.mdc-list-item:not(.mdc-list-item--disabled).mdc-ripple-upgraded) { + @include mdc-feature-targets($feat-color) { + --mdc-ripple-fg-opacity: #{$press-opacity}; + } + } + + // selected and active + + $selected-opacity: mdc-states-opacity(primary, selected); + $activated-opacity: mdc-states-opacity(primary, activated); + $states-color-primary: primary; + $hover-opacity-primary: mdc-states-opacity($states-color-primary, hover); + $focus-opacity-primary: mdc-states-opacity($states-color-primary, focus); + $press-opacity-primary: mdc-states-opacity($states-color-primary, press); + + // selected + + ::slotted([selected]:not(.mdc-list-item--disabled)) { + &::before { + // Opacity falls under color because the chosen opacity is color-dependent. + @include mdc-feature-targets($feat-color) { + opacity: $selected-opacity; + } + } + + @include mdc-ripple-target-selector("&") { + @include mdc-states-base-color(primary, $query); + } + } + + // Hover-selected + + ::slotted([selected].mdc-list-item:not(.mdc-list-item--disabled):hover)::before { + // Opacity falls under color because the chosen opacity is color-dependent + // in typical usage + @include mdc-feature-targets($feat-color) { + opacity: $hover-opacity-primary + $selected-opacity; + } + } + + // Focus-selected + + ::slotted([selected].mdc-list-item:not(.mdc-list-item--disabled).mdc-ripple-upgraded--background-focused), + ::slotted([selected].mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded):focus) { + &::before { + @include mdc-states-focus-opacity-properties_( + $opacity: $focus-opacity-primary + $selected-opacity, $query: $query); + } + } + + // Press-selected + ::slotted([selected].mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded):active) { + &::after { + // Opacity falls under color because the chosen opacity is color-dependent in typical usage + @include mdc-feature-targets($feat-color) { + opacity: $press-opacity-primary + $selected-opacity; + } + } + } + + ::slotted([selected].mdc-list-item:not(.mdc-list-item--disabled).mdc-ripple-upgraded) { + @include mdc-feature-targets($feat-color) { + --mdc-ripple-fg-opacity: #{$press-opacity-primary + $selected-opacity}; + } + } + + // Activated + + ::slotted(.mdc-list-item--activated:not(.mdc-list-item--disabled)) { + &::before { + @include mdc-feature-targets($feat-color) { + opacity: $activated-opacity; + } + } + + @include mdc-ripple-target-selector("&") { + @include mdc-states-base-color(primary, $query); + } + } + + // Hover-activated + + ::slotted(.mdc-list-item--activated.mdc-list-item:not(.mdc-list-item--disabled):hover)::before { + // Opacity falls under color because the chosen opacity is color-dependent + // in typical usage + @include mdc-feature-targets($feat-color) { + opacity: $hover-opacity-primary + $activated-opacity; + } + } + + // Focus-activated + + ::slotted(.mdc-list-item--activated.mdc-list-item:not(.mdc-list-item--disabled).mdc-ripple-upgraded--background-focused), + ::slotted(.mdc-list-item--activated.mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded):focus) { + &::before { + @include mdc-states-focus-opacity-properties_( + $opacity: $focus-opacity-primary + $activated-opacity, $query: $query); + } + } + + // Press-activated + ::slotted(.mdc-list-item--activated.mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded):active) { + &::after { + // Opacity falls under color because the chosen opacity is color-dependent in typical usage + @include mdc-feature-targets($feat-color) { + opacity: $press-opacity-primary + $activated-opacity; + } + } + } + + ::slotted(.mdc-list-item--activated.mdc-list-item:not(.mdc-list-item--disabled).mdc-ripple-upgraded) { + @include mdc-feature-targets($feat-color) { + --mdc-ripple-fg-opacity: #{$press-opacity-primary + $activated-opacity}; + } + } +} \ No newline at end of file diff --git a/packages/select/src/mwc-list-item-ponyfill.ts b/packages/select/src/mwc-list-item-ponyfill.ts deleted file mode 100644 index 7d8796f1b4..0000000000 --- a/packages/select/src/mwc-list-item-ponyfill.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** -@license -Copyright 2019 Google Inc. All Rights Reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -const focusableChildSelector = ` - button:not(:disabled), - a, - .radio:not([disabled]), - .checkbox:not([disabled]), - .tabbable:not([disabled])`; -interface HasChecked extends Element { - checked: boolean; -} - -export const text = (listItem: Element) => { - const textContent = listItem.textContent; - - return textContent ? textContent.trim() : ''; -}; - -export const value = (listItem: Element) => { - return listItem.getAttribute('data-value') || ''; -}; - -export const controlTabIndex = (listItem: Element, tabIndex: string) => { - const tabbables = listItem.querySelectorAll(focusableChildSelector); - tabbables.forEach((element) => element.setAttribute('tabindex', tabIndex)); -}; - -const getCheckbox = (listItem: Element) => { - return listItem.querySelector( - 'input[type="checkbox"]:not(:disabled),.checkbox:not([disabled])') as - Element | - HasChecked | null; -}; - -const getRadio = (listItem: Element) => { - return listItem.querySelector( - 'input[type="radio"]:not(:disabled),.radio:not([disabled])') as - Element | - HasChecked | null; -}; - -export const hasCheckbox = (listItem: Element) => { - return !!getCheckbox(listItem); -}; - -export const hasRadio = (listItem: Element) => { - return !!getRadio(listItem); -}; - -export const isChecked = (listItem: Element) => { - const checkbox = getCheckbox(listItem); - return checkbox && 'checked' in checkbox ? checkbox.checked : false; -}; - -export const setChecked = (listItem: Element, isChecked: boolean) => { - const checkbox = getCheckbox(listItem); - const radio = getRadio(listItem); - - if (checkbox && 'checked' in checkbox) { - checkbox.checked = isChecked; - } - - if (radio && 'checked' in radio) { - radio.checked = isChecked; - } -}; - -export const hasClass = (listItem: Element, className: string) => - listItem.classList.contains(className); - -export const init = (listItem: Element) => { - if (!listItem.hasAttribute('tabindex')) { - listItem.setAttribute('tabindex', '-1'); - } - - controlTabIndex(listItem, '-1'); -}; diff --git a/packages/select/src/mwc-list-ponyfill.ts b/packages/select/src/mwc-list-ponyfill.ts deleted file mode 100644 index 16111cc2b7..0000000000 --- a/packages/select/src/mwc-list-ponyfill.ts +++ /dev/null @@ -1,146 +0,0 @@ -/** - @license - Copyright 2019 Google Inc. All Rights Reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - */ -import MDCListFoundation from '@material/list/foundation'; - -import * as mwcListItem from './mwc-list-item-ponyfill'; - -export const assignedElements = (list: Element): Element[] => { - const slot = list.querySelector('slot'); - - if (slot) { - return slot.assignedNodes({flatten: true}) - .filter((node) => (node.nodeType === Node.ELEMENT_NODE)) as - Element[]; - } - - return []; -}; - -export const selected = (list: Element) => { - const children = assignedElements(list); - for (const child of children) { - const selected = child.classList.contains('mdc-list-item--selected') ? - child : - child.querySelector('.mdc-list-item--selected'); - if (selected) { - return selected; - } - } - - return null; -}; - -export const select = (list: Element, itemToSelect: Element) => { - const previouslySelected = selected(list); - - if (previouslySelected) { - previouslySelected.classList.remove('mdc-list-item--selected'); - previouslySelected.removeAttribute('aria-selected'); - } - - itemToSelect.classList.add('mdc-list-item--selected'); - itemToSelect.removeAttribute('aria-selected'); -}; - -export const items = (list: Element): Element[] => { - const nodes = assignedElements(list); - const listItems = - nodes - .map((element) => { - if (element.classList.contains('mdc-list-item')) { - return element; - } - - return Array.from(element.querySelectorAll('.mdc-list-item')); - }) - .reduce((listItems, listItemResult) => { - return listItems.concat(listItemResult); - }, []); - - return listItems; -}; - -export const mdcRoot = (list: Element) => { - return list as HTMLElement; -}; - -export const getItemAtIndex = - (list: Element, index: number): Element|undefined => { - const elements = items(list); - - return elements[index] as Element | undefined; - }; - -export const getSlottedActiveElement = (list: Element): Element|null => { - const first = getItemAtIndex(list, 0); - if (!first) { - return null; - } - - const root = first.getRootNode() as unknown as DocumentOrShadowRoot; - return root ? root.activeElement : null; -}; - -export const doContentsHaveFocus = (list: Element): boolean => { - const activeElement = getSlottedActiveElement(list); - if (!activeElement) { - return false; - } - - const elements = assignedElements(list); - - return elements.reduce((isContained: boolean, listItem) => { - return isContained || listItem === activeElement || - listItem.contains(activeElement); - }, false); -}; - -export const wrapFocus = - (foundation: MDCListFoundation, wrapFocus: boolean) => { - foundation.setWrapFocus(wrapFocus); - }; - -export const getIndexOfElement = (list: Element, element: Element) => { - const elements = items(list); - - return elements.indexOf(element); -}; - -export const getIndexOfTarget = (list: Element, evt: Event) => { - const elements = items(list); - const path = evt.composedPath(); - - for (const pathItem of path) { - const index = elements.indexOf(pathItem as Element); - if (index !== -1) { - return index; - } - } - - return -1; -}; - -export const layout = (list: Element, foundation: MDCListFoundation) => { - const elements = items(list); - - for (const element of elements) { - mwcListItem.init(element); - } - - foundation.setUseActivatedClass(true); - foundation.layout(); -}; diff --git a/packages/select/src/mwc-menu-ponyfill.ts b/packages/select/src/mwc-menu-ponyfill.ts index 2b13aafe8b..4a4e0aa2f4 100644 --- a/packages/select/src/mwc-menu-ponyfill.ts +++ b/packages/select/src/mwc-menu-ponyfill.ts @@ -15,10 +15,10 @@ See the License for the specific language governing permissions and limitations under the License. */ import {Corner} from '@material/menu-surface/constants'; -import {isNodeElement} from '@material/mwc-base/utils'; import MDCMenuSurfaceFoundation from '@material/menu-surface/foundation'; import {MDCMenuDistance} from '@material/menu-surface/types'; import {getTransformPropertyName} from '@material/menu-surface/util'; +import {isNodeElement} from '@material/mwc-base/utils'; export const open = (foundation: MDCMenuSurfaceFoundation) => { foundation.open(); diff --git a/packages/select/src/mwc-select-base.ts b/packages/select/src/mwc-select-base.ts index 15570f5959..13639323a8 100644 --- a/packages/select/src/mwc-select-base.ts +++ b/packages/select/src/mwc-select-base.ts @@ -15,30 +15,29 @@ See the License for the specific language governing permissions and limitations under the License. */ import '@material/mwc-notched-outline'; +import '@material/mwc-list'; import {closest} from '@material/dom/ponyfill'; import {MDCFloatingLabelFoundation} from '@material/floating-label/foundation.js'; import {MDCLineRippleFoundation} from '@material/line-ripple/foundation.js'; -import {MDCListAdapter} from '@material/list/adapter'; -import MDCListFoundation from '@material/list/foundation.js'; import {MDCMenuSurfaceAdapter} from '@material/menu-surface/adapter'; import MDCMenuSurfaceFoundation from '@material/menu-surface/foundation.js'; import {MDCMenuAdapter} from '@material/menu/adapter'; import MDCMenuFoundation from '@material/menu/foundation.js'; import {addHasRemoveClass, FormElement, observer} from '@material/mwc-base/form-element.js'; +import {isNodeElement, slotActiveElement, doesSlotContainElement} from '@material/mwc-base/utils'; import {floatingLabel, FloatingLabel} from '@material/mwc-floating-label'; import {lineRipple, LineRipple} from '@material/mwc-line-ripple'; +import {List} from '@material/mwc-list'; +import {ListItemBase} from '@material/mwc-list/mwc-list-item-base'; import {NotchedOutline} from '@material/mwc-notched-outline'; import {MDCSelectAdapter} from '@material/select/adapter'; import MDCSelectFoundation from '@material/select/foundation.js'; -import {html, property, PropertyValues, query, TemplateResult} from 'lit-element'; +import {html, property, query, TemplateResult} from 'lit-element'; import {classMap} from 'lit-html/directives/class-map'; -import * as mwcListItem from './mwc-list-item-ponyfill'; -import * as mwcList from './mwc-list-ponyfill'; import * as mwcMenu from './mwc-menu-ponyfill'; import {menuAnchor} from './mwc-menu-surface-anchor-directive'; -import {isNodeElement} from '@material/mwc-base/utils'; // must be done to get past lit-analyzer checks declare global { @@ -53,8 +52,6 @@ export abstract class SelectBase extends FormElement { protected mdcMenuFoundation: MDCMenuFoundation|undefined; - protected mdcListFoundation: MDCListFoundation|undefined; - protected mdcMenuSurfaceFoundation: MDCMenuSurfaceFoundation|undefined; protected readonly mdcFoundationClass = MDCSelectFoundation; @@ -63,7 +60,7 @@ export abstract class SelectBase extends FormElement { @query('.formElement') protected formElement!: HTMLDivElement; - @query('#mainSlot') protected slotElement!: HTMLSlotElement|null; + @query('slot') protected slotElement!: HTMLSlotElement|null; @query('select') protected nativeSelectElement!: HTMLSelectElement|null; @@ -77,7 +74,7 @@ export abstract class SelectBase extends FormElement { @query('.mdc-menu') protected menuElement!: HTMLDivElement|null; - @query('.mdc-list') protected listElement!: HTMLDivElement|null; + @query('mwc-list') protected listElement!: List|null; @query('.mdc-select__selected-text') protected selectedTextElement!: HTMLDivElement|null; @@ -113,6 +110,7 @@ export abstract class SelectBase extends FormElement { })[] = []; protected onBodyClickBound: (evt: MouseEvent) => void = () => {}; protected _outlineUpdateComplete: null|Promise = null; + protected _listUpdateComplete: null|Promise = null; render() { let outlinedOrUnderlined = html``; @@ -156,14 +154,9 @@ export abstract class SelectBase extends FormElement { class="mdc-select__menu mdc-menu mdc-menu-surface" @selected=${this.onSelected} @action=${this.menuOnAction}> -
          + -
        +
      `; } @@ -210,7 +203,6 @@ export abstract class SelectBase extends FormElement { } createAdapter(): MDCSelectAdapter { - this.createListFoundation(); this.createMenuSurfaceFoundation(); this.createMenuFoundation(); @@ -233,7 +225,7 @@ export abstract class SelectBase extends FormElement { return null; } - return mwcList.selected(listElement); + return listElement.selected; }, hasLabel: () => { return !!this.label; @@ -325,8 +317,9 @@ export abstract class SelectBase extends FormElement { } }, setMenuWrapFocus: (wrapFocus) => { - if (this.mdcListFoundation) { - mwcList.wrapFocus(this.mdcListFoundation, wrapFocus); + const listElement = this.listElement; + if (listElement) { + listElement.wrapFocus(wrapFocus); } }, setAttributeAtIndex: (index: number, attr: string, value: string) => { @@ -335,7 +328,7 @@ export abstract class SelectBase extends FormElement { return; } - const element = mwcList.getItemAtIndex(listElement, index); + const element = listElement.items[index]; if (!element) { return; @@ -349,7 +342,7 @@ export abstract class SelectBase extends FormElement { return; } - const element = mwcList.getItemAtIndex(listElement, index); + const element = listElement.items[index]; if (!element) { return; @@ -363,7 +356,7 @@ export abstract class SelectBase extends FormElement { return; } - const element = mwcList.getItemAtIndex(listElement, index); + const element = listElement.items[index]; if (!element) { return; @@ -375,8 +368,7 @@ export abstract class SelectBase extends FormElement { const listElement = this.listElement; if (listElement) { - const elements = mwcList.items(listElement); - return elements.length; + return listElement.items.length; } return 0; @@ -388,9 +380,9 @@ export abstract class SelectBase extends FormElement { return []; } - const items = mwcList.items(listElement); + const items = listElement.items; - return items.map((item) => mwcListItem.value(item)); + return items.map((item) => item.value); }, getMenuItemTextAtIndex: (index) => { const listElement = this.listElement; @@ -398,16 +390,17 @@ export abstract class SelectBase extends FormElement { return ''; } - const element = mwcList.getItemAtIndex(listElement, index); + const element = listElement.items[index]; if (!element) { return ''; } - return element.textContent as string; + return element.text; }, getMenuItemAttr: (menuItem) => { - return mwcListItem.value(menuItem); + const listItem = menuItem as ListItemBase; + return listItem.value; }, addClassAtIndex: (index, className) => { const listElement = this.listElement; @@ -416,7 +409,7 @@ export abstract class SelectBase extends FormElement { return; } - const element = mwcList.getItemAtIndex(listElement, index); + const element = listElement.items[index]; if (!element) { return; @@ -431,7 +424,7 @@ export abstract class SelectBase extends FormElement { return; } - const element = mwcList.getItemAtIndex(listElement, index); + const element = listElement.items[index]; if (!element) { return; @@ -442,182 +435,6 @@ export abstract class SelectBase extends FormElement { }; } - createListFoundation() { - if (!this.listElement) { - return; - } - - if (this.mdcListFoundation) { - this.mdcListFoundation.destroy(); - } - - const mdcListAdapter: MDCListAdapter = { - getListItemCount: () => { - if (this.listElement) { - const elements = mwcList.items(this.listElement); - return elements.length; - } - - return 0; - }, - getFocusedElementIndex: () => { - if (!this.listElement) { - return -1; - } - - const elements = mwcList.items(this.listElement); - - if (!elements.length) { - return -1; - } - - const activeElement = mwcList.getSlottedActiveElement(this.listElement); - - if (!activeElement) { - return -1; - } - - return elements.indexOf(activeElement); - }, - getAttributeForElementIndex: (index, attr) => { - const listElement = this.listElement; - if (!listElement) { - return ''; - } - - const element = mwcList.getItemAtIndex(listElement, index); - return element ? element.getAttribute(attr) : ''; - }, - setAttributeForElementIndex: (index, attr, val) => { - if (!this.listElement) { - return; - } - - const element = mwcList.getItemAtIndex(this.listElement, index); - - if (element) { - element.setAttribute(attr, val); - } - }, - addClassForElementIndex: (index, className) => { - if (!this.listElement) { - return; - } - - const element = mwcList.getItemAtIndex(this.listElement, index); - if (element) { - element.classList.add(className); - } - }, - removeClassForElementIndex: (index, className) => { - if (!this.listElement) { - return; - } - - const element = mwcList.getItemAtIndex(this.listElement, index); - if (element) { - element.classList.remove(className); - } - }, - focusItemAtIndex: (index) => { - if (!this.listElement) { - return; - } - - const element = mwcList.getItemAtIndex(this.listElement, index); - if (element && isNodeElement(element)) { - (element as HTMLElement).focus(); - } - }, - setTabIndexForListItemChildren: (index, tabIndex) => { - if (!this.listElement) { - return; - } - - const element = mwcList.getItemAtIndex(this.listElement, index); - if (element) { - mwcListItem.controlTabIndex(element, tabIndex); - } - }, - hasCheckboxAtIndex: (index) => { - if (!this.listElement) { - return false; - } - - const element = mwcList.getItemAtIndex(this.listElement, index); - return element ? mwcListItem.hasCheckbox(element) : false; - }, - hasRadioAtIndex: (index) => { - if (!this.listElement) { - return false; - } - - const element = mwcList.getItemAtIndex(this.listElement, index); - return element ? mwcListItem.hasRadio(element) : false; - }, - isCheckboxCheckedAtIndex: (index) => { - if (!this.listElement) { - return false; - } - - const element = mwcList.getItemAtIndex(this.listElement, index); - return element ? mwcListItem.hasRadio(element) : false; - }, - setCheckedCheckboxOrRadioAtIndex: (index, isChecked) => { - if (!this.listElement) { - return; - } - - const element = mwcList.getItemAtIndex(this.listElement, index); - if (element) { - mwcListItem.setChecked(element, isChecked); - } - }, - notifyAction: (index) => { - if (!this.listElement) { - return; - } - - const init: CustomEventInit = {bubbles: true}; - init.detail = {index}; - const ev = new CustomEvent('action', init); - this.listElement.dispatchEvent(ev); - }, - isFocusInsideList: () => { - if (!this.listElement) { - return false; - } - - return mwcList.doContentsHaveFocus(this.listElement); - }, - isRootFocused: () => { - if (!this.listElement) { - return false; - } - - const mdcRoot = mwcList.mdcRoot(this.listElement); - const root = mdcRoot.getRootNode() as unknown as DocumentOrShadowRoot; - return root.activeElement === mdcRoot; - }, - listItemAtIndexHasClass: (index, className) => { - if (!this.listElement) { - return false; - } - - const item = mwcList.getItemAtIndex(this.listElement, index); - - if (!item) { - return false; - } - - return mwcListItem.hasClass(item, className); - }, - }; - - this.mdcListFoundation = new MDCListFoundation(mdcListAdapter); - this.mdcListFoundation.init(); - } - createMenuFoundation() { if (!this.menuElement) { return; @@ -629,11 +446,12 @@ export abstract class SelectBase extends FormElement { const mdcMenuAdapter: MDCMenuAdapter = { addClassToElementAtIndex: (index, className) => { - if (!this.listElement) { + const listElement = this.listElement; + if (!listElement) { return; } - const element = mwcList.getItemAtIndex(this.listElement, index); + const element = listElement.items[index]; if (!element) { return; @@ -642,11 +460,12 @@ export abstract class SelectBase extends FormElement { element.classList.add(className); }, removeClassFromElementAtIndex: (index, className) => { - if (!this.listElement) { + const listElement = this.listElement; + if (!listElement) { return; } - const element = mwcList.getItemAtIndex(this.listElement, index); + const element = listElement.items[index]; if (!element) { return; @@ -655,11 +474,12 @@ export abstract class SelectBase extends FormElement { element.classList.remove(className); }, addAttributeToElementAtIndex: (index, attr, value) => { - if (!this.listElement) { + const listElement = this.listElement; + if (!listElement) { return; } - const element = mwcList.getItemAtIndex(this.listElement, index); + const element = listElement.items[index]; if (!element) { return; @@ -668,11 +488,12 @@ export abstract class SelectBase extends FormElement { element.setAttribute(attr, value); }, removeAttributeFromElementAtIndex: (index, attr) => { - if (!this.listElement) { + const listElement = this.listElement; + if (!listElement) { return; } - const element = mwcList.getItemAtIndex(this.listElement, index); + const element = listElement.items[index]; if (!element) { return; @@ -688,8 +509,9 @@ export abstract class SelectBase extends FormElement { } }, getElementIndex: (element) => { - if (this.listElement) { - return mwcList.items(this.listElement).indexOf(element); + const listElement = this.listElement; + if (listElement) { + return listElement.items.indexOf(element as ListItemBase); } return -1; @@ -705,18 +527,20 @@ export abstract class SelectBase extends FormElement { this.menuElement.dispatchEvent(ev); }, getMenuItemCount: () => { - if (!this.listElement) { + const listElement = this.listElement; + if (!listElement) { return 0; } - return mwcList.items(this.listElement).length; + return listElement.items.length; }, focusItemAtIndex: (index) => { - if (!this.listElement) { + const listElement = this.listElement; + if (!listElement) { return; } - const element = mwcList.getItemAtIndex(this.listElement, index); + const element = listElement.items[index]; if (element && isNodeElement(element)) { (element as HTMLElement).focus(); @@ -728,12 +552,13 @@ export abstract class SelectBase extends FormElement { } }, getSelectedSiblingOfItemAtIndex: (index) => { - if (!this.listElement) { + const listElement = this.listElement; + + if (!listElement) { return -1; } - const elementAtIndex = - mwcList.getItemAtIndex(this.listElement, index); + const elementAtIndex = listElement.items[index]; if (!elementAtIndex) { return -1; @@ -747,23 +572,24 @@ export abstract class SelectBase extends FormElement { } const selectedItemEl = - selectionGroupEl.querySelector('.mdc-menu-item--selected'); + selectionGroupEl.querySelector('[selected]') as ListItemBase | null; if (!selectedItemEl) { return -1; } - const elements = mwcList.items(this.listElement); + const elements = listElement.items; return elements.indexOf(selectedItemEl); }, isSelectableItemAtIndex: (index) => { - if (!this.listElement) { + const listElement = this.listElement; + + if (!listElement) { return false; } - const elementAtIndex = - mwcList.getItemAtIndex(this.listElement, index); + const elementAtIndex = listElement.items[index]; if (!elementAtIndex) { return false; @@ -860,28 +686,32 @@ export abstract class SelectBase extends FormElement { }, restoreFocus: () => { const menuElement = this.menuElement; + const slotElement = this.slotElement; - if (!menuElement) { + if (!slotElement || !menuElement) { return; } - const activeElement = mwcMenu.shadowRoot(menuElement).activeElement; + const activeElement = slotActiveElement(slotElement); if (!activeElement) { return; } - const mdcRoot = mwcMenu.mdcRoot(menuElement); + const menuHasFocus = doesSlotContainElement(slotElement, activeElement); + + if (!menuHasFocus) { + return; + } + const previousFocus = mwcMenu.getPreviousFocus(menuElement); - if (!previousFocus || !mdcRoot) { + if (!previousFocus) { return; } - if (mdcRoot.contains(previousFocus)) { - if ('focus' in previousFocus) { - previousFocus.focus(); - } + if ('focus' in previousFocus) { + previousFocus.focus(); } }, getInnerDimensions: () => { @@ -953,13 +783,6 @@ export abstract class SelectBase extends FormElement { this.mdcMenuSurfaceFoundation.init(); } - updated(changedProperties: PropertyValues) { - if (changedProperties.has('value') && - changedProperties.get('value') !== undefined) { - this.mdcFoundation.setValue(this.value); - } - } - protected menuSurfaceOnKeydown(evt: KeyboardEvent) { if (this.mdcMenuSurfaceFoundation) { this.mdcMenuSurfaceFoundation.handleKeydown(evt); @@ -988,8 +811,9 @@ export abstract class SelectBase extends FormElement { } protected menuOnAction(evt: CustomEvent<{index: number}>) { - if (this.mdcMenuFoundation && this.listElement) { - const el = mwcList.getItemAtIndex(this.listElement, evt.detail.index); + const listElement = this.listElement; + if (this.mdcMenuFoundation && listElement) { + const el = listElement.items[evt.detail.index]; if (el) { this.mdcMenuFoundation.handleItemAction(el); } @@ -1002,61 +826,30 @@ export abstract class SelectBase extends FormElement { } } - private listOnFocusin(evt: FocusEvent) { - if (this.mdcListFoundation && this.listElement) { - const index = mwcList.getIndexOfTarget(this.listElement, evt); - this.mdcListFoundation.handleFocusIn(evt, index); - } - } - - private listOnFocusout(evt: FocusEvent) { - if (this.mdcListFoundation && this.listElement) { - const index = mwcList.getIndexOfTarget(this.listElement, evt); - this.mdcListFoundation.handleFocusOut(evt, index); - } - } - - private listOnKeydown(evt: KeyboardEvent) { - if (this.mdcListFoundation && this.listElement) { - const index = mwcList.getIndexOfTarget(this.listElement, evt); - const target = evt.target as Element; - const elements = mwcList.items(this.listElement); - const isRootListItem = elements ? elements.indexOf(target) !== -1 : false; - this.mdcListFoundation.handleKeydown(evt, isRootListItem, index); - } - } - - private listOnClick(evt: MouseEvent) { - if (this.mdcListFoundation && this.listElement) { - const index = mwcList.getIndexOfTarget(this.listElement, evt); - const target = evt.target as Element | null; - const toggleCheckbox = target && 'getAttribute' in target ? - target.getAttribute('role') === 'radio' && - target.getAttribute('aria-checked') === 'true' : - false; - this.mdcListFoundation.handleClick(index, toggleCheckbox); - } - } - async _getUpdateComplete() { await super._getUpdateComplete(); - await this._outlineUpdateComplete; + await Promise.all([ + this._outlineUpdateComplete, + this._listUpdateComplete, + ]); } async firstUpdated() { + const listElement = this.listElement; const outlineElement = this.outlineElement; if (outlineElement) { this._outlineUpdateComplete = outlineElement.updateComplete; await this._outlineUpdateComplete; } + if (listElement) { + this._listUpdateComplete = listElement.updateComplete; + await this._listUpdateComplete; + } + super.firstUpdated(); this.mdcFoundation.setDisabled(this.disabled); - const listElement = this.listElement; - if (listElement && this.mdcListFoundation) { - mwcList.layout(listElement, this.mdcListFoundation); - } // if (this.validateOnInitialRender) { // this.reportValidity(); @@ -1111,11 +904,12 @@ export abstract class SelectBase extends FormElement { listener.name, listener.cb as EventListenerOrEventListenerObject); } - if (menuElement) { - const selected = mwcList.selected(menuElement); + if (menuElement && listElement) { + const selected = listElement.selected; if (selected) { - const index = mwcList.getIndexOfElement(menuElement, selected); + const listIndex = listElement.index; + const index = listIndex instanceof Array ? listIndex[0] : listIndex; if (index !== -1 && this.mdcFoundation) { this.mdcFoundation.setSelectedIndex(index); } diff --git a/packages/select/src/mwc-select.scss b/packages/select/src/mwc-select.scss index c0c99a8f06..87d5994713 100644 --- a/packages/select/src/mwc-select.scss +++ b/packages/select/src/mwc-select.scss @@ -18,7 +18,6 @@ limitations under the License. @import "@material/select/mdc-select.scss"; @import "@material/menu/mdc-menu.scss"; @import "@material/menu-surface/mdc-menu-surface.scss"; -@import "@material/list/mdc-list.scss"; :host { display: inline-block; @@ -60,372 +59,6 @@ limitations under the License. } } -// listitem -$query: mdc-feature-all(); -$feat-structure: mdc-feature-create-target($query, structure); -$feat-color: mdc-feature-create-target($query, color); -$feat-animation: mdc-feature-create-target($query, animation); -$item-primary-text-baseline-height: 32px; -$item-secondary-text-baseline-height: 20px; -$dense-item-primary-text-baseline-height: 24px; - -.mdc-list ::slotted(.mdc-list-item) { - @include mdc-list-item-base_; - - outline: none; - - $height: mdc-density-prop-value( - $density-config: $mdc-list-single-line-density-config, - $density-scale: $mdc-list-single-line-density-scale, - $property-name: height, - ); - - @include mdc-list-single-line-height($height, $query: $query); -} - -// "Selected" is ephemeral and likely to change soon. E.g., selecting one or more photos to share in Google Photos. -// "Activated" is more permanent. E.g., the currently highlighted navigation destination in a drawer. -::slotted(.mdc-list-item--selected), -::slotted(.mdc-list-item--activated) { - @include mdc-list-item-primary-text-ink-color(primary, $query); - @include mdc-list-item-graphic-ink-color(primary, $query); -} - -::slotted(.mdc-list-item__graphic) { - @include mdc-feature-targets($feat-structure) { - @include mdc-list-graphic-size_(24px); - - flex-shrink: 0; - align-items: center; - justify-content: center; - fill: currentColor; - } -} - -// Extra specificity is to override .material-icons display style if used in -// conjunction with mdc-list-item__graphic -// stylelint-disable plugin/selector-bem-pattern -.mdc-list ::slotted(.mdc-list-item__graphic) { - @include mdc-feature-targets($feat-structure) { - display: inline-flex; - } -} -// stylelint-enable plugin/selector-bem-pattern - -::slotted(.mdc-list-item__meta) { - // stylelint-disable selector-class-pattern - &:not(.material-icons) { - @include mdc-typography(caption, $query); - } - // stylelint-enable selector-class-pattern - - @include mdc-feature-targets($feat-structure) { - @include mdc-rtl-reflexive-property(margin, auto, 0, "::slotted(.mdc-list-item)"); - } -} - -::slotted(.mdc-list-item__text) { - @include mdc-typography-overflow-ellipsis($query); -} - -// Disable interaction on label elements that may automatically -// toggle corresponding checkbox / radio input. -::slotted(.mdc-list-item__text)[for] { - @include mdc-feature-targets($feat-structure) { - pointer-events: none; - } -} - -::slotted(.mdc-list-item__primary-text) { - @include mdc-typography-overflow-ellipsis($query); - @include mdc-typography-baseline-top($item-primary-text-baseline-height, $query); - @include mdc-typography-baseline-bottom($item-secondary-text-baseline-height, $query); - - @include mdc-feature-targets($feat-structure) { - display: block; - } - - // stylelint-disable plugin/selector-bem-pattern - .mdc-list--dense & { - @include mdc-typography-baseline-top($dense-item-primary-text-baseline-height, $query); - @include mdc-typography-baseline-bottom($item-secondary-text-baseline-height, $query); - } - // stylelint-enable plugin/selector-bem-pattern -} - -::slotted(.mdc-list-item__secondary-text) { - @include mdc-typography(body2, $query); - @include mdc-typography-overflow-ellipsis($query); - @include mdc-typography-baseline-top($item-secondary-text-baseline-height, $query); - - @include mdc-feature-targets($feat-structure) { - display: block; - } - - // stylelint-disable plugin/selector-bem-pattern - .mdc-list--dense & { - @include mdc-typography-baseline-top($item-secondary-text-baseline-height, $query); - - @include mdc-feature-targets($feat-structure) { - font-size: inherit; - } - } - // stylelint-enable plugin/selector-bem-pattern -} - -// stylelint-disable plugin/selector-bem-pattern -.mdc-list--dense ::slotted(.mdc-list-item) { - @include mdc-feature-targets($feat-structure) { - height: 40px; - } -} - -.mdc-list--dense ::slotted(.mdc-list-item__graphic) { - @include mdc-feature-targets($feat-structure) { - @include mdc-list-graphic-size_(20px); - } -} - -.mdc-list--avatar-list ::slotted(.mdc-list-item) { - @include mdc-feature-targets($feat-structure) { - height: 56px; - } -} - -.mdc-list--avatar-list ::slotted(.mdc-list-item__graphic) { - @include mdc-feature-targets($feat-structure) { - @include mdc-list-graphic-size_(40px); - - border-radius: 50%; - } -} - -.mdc-list--two-line ::slotted(.mdc-list-item__text) { - @include mdc-feature-targets($feat-structure) { - align-self: flex-start; - } -} - -.mdc-list--two-line ::slotted(.mdc-list-item) { - @include mdc-feature-targets($feat-structure) { - height: 72px; - } -} - -.mdc-list--two-line.mdc-list--dense ::slotted(.mdc-list-item), -.mdc-list--avatar-list.mdc-list--dense ::slotted(.mdc-list-item) { - @include mdc-feature-targets($feat-structure) { - height: 60px; - } -} - -.mdc-list--avatar-list.mdc-list--dense ::slotted(.mdc-list-item__graphic) { - @include mdc-feature-targets($feat-structure) { - @include mdc-list-graphic-size_(36px); - } -} - -// Only change mouse cursor for interactive list items which are not disabled. -:not(.mdc-list--non-interactive) > :not(::slotted(.mdc-list-item--disabled))::slotted(.mdc-list-item) { - @include mdc-feature-targets($feat-structure) { - cursor: pointer; - } -} - -// Override anchor tag styles for the use-case of a list being used for navigation -// stylelint-disable selector-max-type,selector-no-qualifying-type -::slotted(a.mdc-list-item) { - @include mdc-feature-targets($feat-structure) { - color: inherit; - text-decoration: none; - } -} - -.mdc-list:not(.mdc-list--non-interactive) { - $states-color: mdc-theme-prop-value(on-surface); - $hover-opacity: mdc-states-opacity($states-color, hover); - $focus-opacity: mdc-states-opacity($states-color, focus); - $press-opacity: mdc-states-opacity($states-color, press); - - ::slotted(.mdc-list-item) { - cursor: pointer; - user-select: none; - } - - ::slotted(.mdc-list-item:not(.mdc-list-item--disabled)) { - - @include mdc-ripple-surface($query); - @include mdc-ripple-radius-bounded($query: $query); - @include mdc-ripple-target-selector("&") { - @include mdc-states-base-color($states-color, $query); - } - } - - ::slotted(.mdc-list-item.mdc-list-item--disabled) { - cursor: default; - } - - // Hover - - ::slotted(.mdc-list-item:not(.mdc-list-item--disabled):hover)::before { - // Opacity falls under color because the chosen opacity is color-dependent - // in typical usage - @include mdc-feature-targets($feat-color) { - opacity: $hover-opacity; - } - } - - // Focus - - ::slotted(.mdc-list-item:not(.mdc-list-item--disabled).mdc-ripple-upgraded--background-focused), - ::slotted(.mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded):focus) { - &::before { - @include mdc-states-focus-opacity-properties_( - $opacity: $focus-opacity, $query: $query); - } - } - - // Press - - ::slotted(.mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded)) { - // Apply press additively by using the ::after pseudo-element - &::after { - @include mdc-feature-targets($feat-animation) { - transition: opacity $mdc-ripple-fade-out-duration linear; - } - } - } - - ::slotted(.mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded):active) { - &::after { - @include mdc-feature-targets($feat-animation) { - transition-duration: $mdc-ripple-fade-in-duration; - } - - // Opacity falls under color because the chosen opacity is color-dependent in typical usage - @include mdc-feature-targets($feat-color) { - opacity: $press-opacity; - } - } - } - - ::slotted(.mdc-list-item:not(.mdc-list-item--disabled).mdc-ripple-upgraded) { - @include mdc-feature-targets($feat-color) { - --mdc-ripple-fg-opacity: #{$press-opacity}; - } - } - - // selected and active - - $selected-opacity: mdc-states-opacity(primary, selected); - $activated-opacity: mdc-states-opacity(primary, activated); - $states-color-primary: primary; - $hover-opacity-primary: mdc-states-opacity($states-color-primary, hover); - $focus-opacity-primary: mdc-states-opacity($states-color-primary, focus); - $press-opacity-primary: mdc-states-opacity($states-color-primary, press); - - // selected - - ::slotted(.mdc-list-item--selected:not(.mdc-list-item--disabled)) { - &::before { - // Opacity falls under color because the chosen opacity is color-dependent. - @include mdc-feature-targets($feat-color) { - opacity: $selected-opacity; - } - } - - @include mdc-ripple-target-selector("&") { - @include mdc-states-base-color(primary, $query); - } - } - - // Hover-selected - - ::slotted(.mdc-list-item--selected.mdc-list-item:not(.mdc-list-item--disabled):hover)::before { - // Opacity falls under color because the chosen opacity is color-dependent - // in typical usage - @include mdc-feature-targets($feat-color) { - opacity: $hover-opacity-primary + $selected-opacity; - } - } - - // Focus-selected - - ::slotted(.mdc-list-item--selected.mdc-list-item:not(.mdc-list-item--disabled).mdc-ripple-upgraded--background-focused), - ::slotted(.mdc-list-item--selected.mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded):focus) { - &::before { - @include mdc-states-focus-opacity-properties_( - $opacity: $focus-opacity-primary + $selected-opacity, $query: $query); - } - } - - // Press-selected - ::slotted(.mdc-list-item--selected.mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded):active) { - &::after { - // Opacity falls under color because the chosen opacity is color-dependent in typical usage - @include mdc-feature-targets($feat-color) { - opacity: $press-opacity-primary + $selected-opacity; - } - } - } - - ::slotted(.mdc-list-item--selected.mdc-list-item:not(.mdc-list-item--disabled).mdc-ripple-upgraded) { - @include mdc-feature-targets($feat-color) { - --mdc-ripple-fg-opacity: #{$press-opacity-primary + $selected-opacity}; - } - } - - // Activated - - ::slotted(.mdc-list-item--activated:not(.mdc-list-item--disabled)) { - &::before { - @include mdc-feature-targets($feat-color) { - opacity: $activated-opacity; - } - } - - @include mdc-ripple-target-selector("&") { - @include mdc-states-base-color(primary, $query); - } - } - - // Hover-activated - - ::slotted(.mdc-list-item--activated.mdc-list-item:not(.mdc-list-item--disabled):hover)::before { - // Opacity falls under color because the chosen opacity is color-dependent - // in typical usage - @include mdc-feature-targets($feat-color) { - opacity: $hover-opacity-primary + $activated-opacity; - } - } - - // Focus-activated - - ::slotted(.mdc-list-item--activated.mdc-list-item:not(.mdc-list-item--disabled).mdc-ripple-upgraded--background-focused), - ::slotted(.mdc-list-item--activated.mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded):focus) { - &::before { - @include mdc-states-focus-opacity-properties_( - $opacity: $focus-opacity-primary + $activated-opacity, $query: $query); - } - } - - // Press-activated - ::slotted(.mdc-list-item--activated.mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded):active) { - &::after { - // Opacity falls under color because the chosen opacity is color-dependent in typical usage - @include mdc-feature-targets($feat-color) { - opacity: $press-opacity-primary + $activated-opacity; - } - } - } - - ::slotted(.mdc-list-item--activated.mdc-list-item:not(.mdc-list-item--disabled).mdc-ripple-upgraded) { - @include mdc-feature-targets($feat-color) { - --mdc-ripple-fg-opacity: #{$press-opacity-primary + $activated-opacity}; - } - } -} - // menu .mdc-menu { From c53d0ab09daa02758ff0216b41e4800673de4bac Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Thu, 9 Jan 2020 17:49:41 -0800 Subject: [PATCH 026/161] slim down list styles --- packages/list/src/mwc-list-item-base.ts | 10 +- packages/list/src/mwc-list-item.scss | 125 ++++++++++++++- packages/list/src/mwc-list-item.ts | 2 +- packages/list/src/mwc-list.scss | 196 ++++++++++-------------- 4 files changed, 211 insertions(+), 122 deletions(-) diff --git a/packages/list/src/mwc-list-item-base.ts b/packages/list/src/mwc-list-item-base.ts index bf0f7faec0..ff2939c99c 100644 --- a/packages/list/src/mwc-list-item-base.ts +++ b/packages/list/src/mwc-list-item-base.ts @@ -53,11 +53,9 @@ export class ListItemBase extends LitElement { render() { return html` -
    • - -
    • `; + `; } protected getControl(): HasChecked|null { @@ -100,5 +98,7 @@ export class ListItemBase extends LitElement { } this.setControlTabIndex('-1'); + + this.toggleAttribute('mwc-list-item', true); } } diff --git a/packages/list/src/mwc-list-item.scss b/packages/list/src/mwc-list-item.scss index ab9f543474..8dab3b8fbf 100644 --- a/packages/list/src/mwc-list-item.scss +++ b/packages/list/src/mwc-list-item.scss @@ -15,9 +15,128 @@ See the License for the specific language governing permissions and limitations under the License. */ -@import "@material/list/mdc-list.scss"; +@import "@material/list/_mixins.scss"; + +// listitem +$query: mdc-feature-all(); +$feat-structure: mdc-feature-create-target($query, structure); +$feat-color: mdc-feature-create-target($query, color); +$feat-animation: mdc-feature-create-target($query, animation); +$item-primary-text-baseline-height: 32px; +$item-secondary-text-baseline-height: 20px; +$dense-item-primary-text-baseline-height: 24px; :host { - display: list-item; + $height: mdc-density-prop-value( + $density-config: $mdc-list-single-line-density-config, + $density-scale: $mdc-list-single-line-density-scale, + $property-name: height, + ); + @include mdc-list-single-line-height($height, $query: $query); + @include mdc-list-item-base_; + outline: none; -} \ No newline at end of file + + $height: mdc-density-prop-value( + $density-config: $mdc-list-single-line-density-config, + $density-scale: $mdc-list-single-line-density-scale, + $property-name: height, + ); + + @include mdc-list-single-line-height($height, $query: $query); +} + +// "Selected" is ephemeral and likely to change soon. E.g., selecting one or more photos to share in Google Photos. +// "Activated" is more permanent. E.g., the currently highlighted navigation destination in a drawer. +:host([selected]), +:host(.mdc-list-item--activated) { + @include mdc-list-item-primary-text-ink-color(primary, $query); + @include mdc-list-item-graphic-ink-color(primary, $query); +} + +:host(.mdc-list-item__graphic) { + @include mdc-feature-targets($feat-structure) { + @include mdc-list-graphic-size_(24px); + + flex-shrink: 0; + align-items: center; + justify-content: center; + fill: currentColor; + } +} + +// Extra specificity is to override .material-icons display style if used in +// conjunction with mdc-list-item__graphic +// stylelint-disable plugin/selector-bem-pattern +:host(.mdc-list-item__graphic) { + @include mdc-feature-targets($feat-structure) { + display: inline-flex; + } +} +// stylelint-enable plugin/selector-bem-pattern + +:host(.mdc-list-item__meta) { + // stylelint-disable selector-class-pattern + &:not(.material-icons) { + @include mdc-typography(caption, $query); + } + // stylelint-enable selector-class-pattern + + @include mdc-feature-targets($feat-structure) { + @include mdc-rtl-reflexive-property(margin, auto, 0, ":host"); + } +} + +:host(.mdc-list-item__text) { + @include mdc-typography-overflow-ellipsis($query); +} + +// Disable interaction on label elements that may automatically +// toggle corresponding checkbox / radio input. +:host(.mdc-list-item__text)[for] { + @include mdc-feature-targets($feat-structure) { + pointer-events: none; + } +} + +:host(.mdc-list-item__primary-text) { + @include mdc-typography-overflow-ellipsis($query); + @include mdc-typography-baseline-top($item-primary-text-baseline-height, $query); + @include mdc-typography-baseline-bottom($item-secondary-text-baseline-height, $query); + + @include mdc-feature-targets($feat-structure) { + display: block; + } +} + +:host(.mdc-list-item__secondary-text) { + @include mdc-typography(body2, $query); + @include mdc-typography-overflow-ellipsis($query); + @include mdc-typography-baseline-top($item-secondary-text-baseline-height, $query); + + @include mdc-feature-targets($feat-structure) { + display: block; + } + + // stylelint-disable plugin/selector-bem-pattern + .mdc-list--dense & { + @include mdc-typography-baseline-top($item-secondary-text-baseline-height, $query); + + @include mdc-feature-targets($feat-structure) { + font-size: inherit; + } + } + // stylelint-enable plugin/selector-bem-pattern +} + + + +// Override anchor tag styles for the use-case of a list being used for navigation +// stylelint-disable selector-max-type,selector-no-qualifying-type +:host(a) { + @include mdc-feature-targets($feat-structure) { + color: inherit; + text-decoration: none; + } +} + diff --git a/packages/list/src/mwc-list-item.ts b/packages/list/src/mwc-list-item.ts index c1f6926713..d04e61c127 100644 --- a/packages/list/src/mwc-list-item.ts +++ b/packages/list/src/mwc-list-item.ts @@ -17,7 +17,7 @@ limitations under the License. import {customElement} from 'lit-element'; -import {style} from './mwc-list-css.js'; +import {style} from './mwc-list-item-css.js'; import {ListItemBase} from './mwc-list-item-base.js'; declare global { diff --git a/packages/list/src/mwc-list.scss b/packages/list/src/mwc-list.scss index b65167b75a..0485472bf1 100644 --- a/packages/list/src/mwc-list.scss +++ b/packages/list/src/mwc-list.scss @@ -15,123 +15,97 @@ See the License for the specific language governing permissions and limitations under the License. */ -@import "@material/list/mdc-list.scss"; +@import "@material/list/_mixins.scss"; -// listitem $query: mdc-feature-all(); -$feat-structure: mdc-feature-create-target($query, structure); + $feat-color: mdc-feature-create-target($query, color); +$feat-structure: mdc-feature-create-target($query, structure); +$feat-typography: mdc-feature-create-target($query, typography); $feat-animation: mdc-feature-create-target($query, animation); + $item-primary-text-baseline-height: 32px; $item-secondary-text-baseline-height: 20px; $dense-item-primary-text-baseline-height: 24px; - -.mdc-list ::slotted(mwc-list-item,.list-item) { - @include mdc-list-item-base_; - - outline: none; - - $height: mdc-density-prop-value( - $density-config: $mdc-list-single-line-density-config, - $density-scale: $mdc-list-single-line-density-scale, - $property-name: height, - ); - - @include mdc-list-single-line-height($height, $query: $query); +$divider-color: if( + mdc-theme-tone($mdc-theme-background) == "dark", + $mdc-list-divider-color-on-dark-bg, + $mdc-list-divider-color-on-light-bg +); + +.mdc-list { + @include mdc-list-base_($query); } -// "Selected" is ephemeral and likely to change soon. E.g., selecting one or more photos to share in Google Photos. -// "Activated" is more permanent. E.g., the currently highlighted navigation destination in a drawer. -::slotted([selected]), -::slotted(.mdc-list-item--activated) { - @include mdc-list-item-primary-text-ink-color(primary, $query); - @include mdc-list-item-graphic-ink-color(primary, $query); -} +@include mdc-list-single-line-density($mdc-list-single-line-density-scale, $query: $query); -::slotted(.mdc-list-item__graphic) { +.mdc-list--dense { @include mdc-feature-targets($feat-structure) { - @include mdc-list-graphic-size_(24px); - - flex-shrink: 0; - align-items: center; - justify-content: center; - fill: currentColor; + padding-top: 4px; + padding-bottom: 4px; + font-size: .812rem; } } +// stylelint-enable selector-max-type,selector-no-qualifying-type -// Extra specificity is to override .material-icons display style if used in -// conjunction with mdc-list-item__graphic -// stylelint-disable plugin/selector-bem-pattern -.mdc-list ::slotted(.mdc-list-item__graphic) { +.mdc-list-divider { @include mdc-feature-targets($feat-structure) { - display: inline-flex; + height: 0; + margin: 0; + border: none; + border-bottom-width: 1px; + border-bottom-style: solid; } } -// stylelint-enable plugin/selector-bem-pattern -::slotted(.mdc-list-item__meta) { - // stylelint-disable selector-class-pattern - &:not(.material-icons) { - @include mdc-typography(caption, $query); - } - // stylelint-enable selector-class-pattern +// Note: ideally we'd be able to hoist this to the top-level `$feat-color`, but doing so +// will cause the `border` declaration on `.mdc-list-divider` above to override it. +@include mdc-list-divider-color($divider-color, $query); +.mdc-list-divider--padded { @include mdc-feature-targets($feat-structure) { - @include mdc-rtl-reflexive-property(margin, auto, 0, "::slotted(mwc-list-item,.list-item)"); + // Leave gaps on each side to match the padding on list items + margin: 0 $mdc-list-side-padding; } } -::slotted(.mdc-list-item__text) { - @include mdc-typography-overflow-ellipsis($query); -} - -// Disable interaction on label elements that may automatically -// toggle corresponding checkbox / radio input. -::slotted(.mdc-list-item__text)[for] { +.mdc-list-divider--inset { @include mdc-feature-targets($feat-structure) { - pointer-events: none; + @include mdc-rtl-reflexive-box(margin, left, $mdc-list-text-offset, ".mdc-list-group"); + + width: calc(100% - #{$mdc-list-text-offset}); } } -::slotted(.mdc-list-item__primary-text) { - @include mdc-typography-overflow-ellipsis($query); - @include mdc-typography-baseline-top($item-primary-text-baseline-height, $query); - @include mdc-typography-baseline-bottom($item-secondary-text-baseline-height, $query); - +.mdc-list-divider--inset.mdc-list-divider--padded { @include mdc-feature-targets($feat-structure) { - display: block; - } - - // stylelint-disable plugin/selector-bem-pattern - .mdc-list--dense & { - @include mdc-typography-baseline-top($dense-item-primary-text-baseline-height, $query); - @include mdc-typography-baseline-bottom($item-secondary-text-baseline-height, $query); + width: calc(100% - #{$mdc-list-text-offset} - #{$mdc-list-side-padding}); } - // stylelint-enable plugin/selector-bem-pattern } -::slotted(.mdc-list-item__secondary-text) { - @include mdc-typography(body2, $query); - @include mdc-typography-overflow-ellipsis($query); - @include mdc-typography-baseline-top($item-secondary-text-baseline-height, $query); - +.mdc-list-group { @include mdc-feature-targets($feat-structure) { - display: block; + // Cancel top/bottom padding on individual lists within group + .mdc-list { + padding: 0; + } } +} - // stylelint-disable plugin/selector-bem-pattern - .mdc-list--dense & { - @include mdc-typography-baseline-top($item-secondary-text-baseline-height, $query); +.mdc-list-group__subheader { + $mdc-list-subheader-virtual-height: 3rem; + $mdc-list-subheader-leading: map-get(map-get($mdc-typography-styles, body1), line-height); + $mdc-list-subheader-margin: ($mdc-list-subheader-virtual-height - $mdc-list-subheader-leading) / 2; - @include mdc-feature-targets($feat-structure) { - font-size: inherit; - } + @include mdc-typography(subtitle1, $query); + + @include mdc-feature-targets($feat-structure) { + margin: $mdc-list-subheader-margin $mdc-list-side-padding; } - // stylelint-enable plugin/selector-bem-pattern } // stylelint-disable plugin/selector-bem-pattern -.mdc-list--dense ::slotted(mwc-list-item,.list-item) { +.mdc-list--dense ::slotted([mwc-list-item]) { @include mdc-feature-targets($feat-structure) { height: 40px; } @@ -143,7 +117,7 @@ $dense-item-primary-text-baseline-height: 24px; } } -.mdc-list--avatar-list ::slotted(mwc-list-item,.list-item) { +.mdc-list--avatar-list ::slotted([mwc-list-item]) { @include mdc-feature-targets($feat-structure) { height: 56px; } @@ -163,14 +137,14 @@ $dense-item-primary-text-baseline-height: 24px; } } -.mdc-list--two-line ::slotted(mwc-list-item,.list-item) { +.mdc-list--two-line ::slotted([mwc-list-item]) { @include mdc-feature-targets($feat-structure) { height: 72px; } } -.mdc-list--two-line.mdc-list--dense ::slotted(mwc-list-item,.list-item), -.mdc-list--avatar-list.mdc-list--dense ::slotted(mwc-list-item,.list-item) { +.mdc-list--two-line.mdc-list--dense ::slotted([mwc-list-item]), +.mdc-list--avatar-list.mdc-list--dense ::slotted([mwc-list-item]) { @include mdc-feature-targets($feat-structure) { height: 60px; } @@ -183,33 +157,24 @@ $dense-item-primary-text-baseline-height: 24px; } // Only change mouse cursor for interactive list items which are not disabled. -:not(.mdc-list--non-interactive) > :not(::slotted(.mdc-list-item--disabled))::slotted(mwc-list-item,.list-item) { +:not(.mdc-list--non-interactive) > :not(::slotted([disabled]))::slotted([mwc-list-item]) { @include mdc-feature-targets($feat-structure) { cursor: pointer; } } -// Override anchor tag styles for the use-case of a list being used for navigation -// stylelint-disable selector-max-type,selector-no-qualifying-type -::slotted(amwc-list-item,.list-item) { - @include mdc-feature-targets($feat-structure) { - color: inherit; - text-decoration: none; - } -} - .mdc-list:not(.mdc-list--non-interactive) { $states-color: mdc-theme-prop-value(on-surface); $hover-opacity: mdc-states-opacity($states-color, hover); $focus-opacity: mdc-states-opacity($states-color, focus); $press-opacity: mdc-states-opacity($states-color, press); - ::slotted(mwc-list-item,.list-item) { + ::slotted([mwc-list-item]) { cursor: pointer; user-select: none; } - ::slotted(.mdc-list-item:not(.mdc-list-item--disabled)) { + ::slotted([mwc-list-item]:not([disabled])) { @include mdc-ripple-surface($query); @include mdc-ripple-radius-bounded($query: $query); @@ -218,13 +183,13 @@ $dense-item-primary-text-baseline-height: 24px; } } - ::slotted(.mdc-list-item.mdc-list-item--disabled) { + ::slotted([mwc-list-item][disabled]) { cursor: default; } // Hover - ::slotted(.mdc-list-item:not(.mdc-list-item--disabled):hover)::before { + ::slotted([mwc-list-item]:not([disabled]):hover)::before { // Opacity falls under color because the chosen opacity is color-dependent // in typical usage @include mdc-feature-targets($feat-color) { @@ -234,8 +199,8 @@ $dense-item-primary-text-baseline-height: 24px; // Focus - ::slotted(.mdc-list-item:not(.mdc-list-item--disabled).mdc-ripple-upgraded--background-focused), - ::slotted(.mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded):focus) { + ::slotted([mwc-list-item]:not([disabled]).mdc-ripple-upgraded--background-focused), + ::slotted([mwc-list-item]:not([disabled]):not(.mdc-ripple-upgraded):focus) { &::before { @include mdc-states-focus-opacity-properties_( $opacity: $focus-opacity, $query: $query); @@ -244,7 +209,7 @@ $dense-item-primary-text-baseline-height: 24px; // Press - ::slotted(.mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded)) { + ::slotted([mwc-list-item]:not([disabled]):not(.mdc-ripple-upgraded)) { // Apply press additively by using the ::after pseudo-element &::after { @include mdc-feature-targets($feat-animation) { @@ -253,7 +218,7 @@ $dense-item-primary-text-baseline-height: 24px; } } - ::slotted(.mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded):active) { + ::slotted([mwc-list-item]:not([disabled]):not(.mdc-ripple-upgraded):active) { &::after { @include mdc-feature-targets($feat-animation) { transition-duration: $mdc-ripple-fade-in-duration; @@ -266,7 +231,7 @@ $dense-item-primary-text-baseline-height: 24px; } } - ::slotted(.mdc-list-item:not(.mdc-list-item--disabled).mdc-ripple-upgraded) { + ::slotted([mwc-list-item]:not([disabled]).mdc-ripple-upgraded) { @include mdc-feature-targets($feat-color) { --mdc-ripple-fg-opacity: #{$press-opacity}; } @@ -283,7 +248,7 @@ $dense-item-primary-text-baseline-height: 24px; // selected - ::slotted([selected]:not(.mdc-list-item--disabled)) { + ::slotted([selected]:not([disabled])) { &::before { // Opacity falls under color because the chosen opacity is color-dependent. @include mdc-feature-targets($feat-color) { @@ -298,7 +263,7 @@ $dense-item-primary-text-baseline-height: 24px; // Hover-selected - ::slotted([selected].mdc-list-item:not(.mdc-list-item--disabled):hover)::before { + ::slotted([selected][mwc-list-item]:not([disabled]):hover)::before { // Opacity falls under color because the chosen opacity is color-dependent // in typical usage @include mdc-feature-targets($feat-color) { @@ -308,8 +273,8 @@ $dense-item-primary-text-baseline-height: 24px; // Focus-selected - ::slotted([selected].mdc-list-item:not(.mdc-list-item--disabled).mdc-ripple-upgraded--background-focused), - ::slotted([selected].mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded):focus) { + ::slotted([selected][mwc-list-item]:not([disabled]).mdc-ripple-upgraded--background-focused), + ::slotted([selected][mwc-list-item]:not([disabled]):not(.mdc-ripple-upgraded):focus) { &::before { @include mdc-states-focus-opacity-properties_( $opacity: $focus-opacity-primary + $selected-opacity, $query: $query); @@ -317,7 +282,7 @@ $dense-item-primary-text-baseline-height: 24px; } // Press-selected - ::slotted([selected].mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded):active) { + ::slotted([selected][mwc-list-item]:not([disabled]):not(.mdc-ripple-upgraded):active) { &::after { // Opacity falls under color because the chosen opacity is color-dependent in typical usage @include mdc-feature-targets($feat-color) { @@ -326,7 +291,7 @@ $dense-item-primary-text-baseline-height: 24px; } } - ::slotted([selected].mdc-list-item:not(.mdc-list-item--disabled).mdc-ripple-upgraded) { + ::slotted([selected][mwc-list-item]:not([disabled]).mdc-ripple-upgraded) { @include mdc-feature-targets($feat-color) { --mdc-ripple-fg-opacity: #{$press-opacity-primary + $selected-opacity}; } @@ -334,7 +299,7 @@ $dense-item-primary-text-baseline-height: 24px; // Activated - ::slotted(.mdc-list-item--activated:not(.mdc-list-item--disabled)) { + ::slotted(.mdc-list-item--activated:not([disabled])) { &::before { @include mdc-feature-targets($feat-color) { opacity: $activated-opacity; @@ -348,7 +313,7 @@ $dense-item-primary-text-baseline-height: 24px; // Hover-activated - ::slotted(.mdc-list-item--activated.mdc-list-item:not(.mdc-list-item--disabled):hover)::before { + ::slotted(.mdc-list-item--activated[mwc-list-item]:not([disabled]):hover)::before { // Opacity falls under color because the chosen opacity is color-dependent // in typical usage @include mdc-feature-targets($feat-color) { @@ -358,8 +323,8 @@ $dense-item-primary-text-baseline-height: 24px; // Focus-activated - ::slotted(.mdc-list-item--activated.mdc-list-item:not(.mdc-list-item--disabled).mdc-ripple-upgraded--background-focused), - ::slotted(.mdc-list-item--activated.mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded):focus) { + ::slotted(.mdc-list-item--activated[mwc-list-item]:not([disabled]).mdc-ripple-upgraded--background-focused), + ::slotted(.mdc-list-item--activated[mwc-list-item]:not([disabled]):not(.mdc-ripple-upgraded):focus) { &::before { @include mdc-states-focus-opacity-properties_( $opacity: $focus-opacity-primary + $activated-opacity, $query: $query); @@ -367,7 +332,7 @@ $dense-item-primary-text-baseline-height: 24px; } // Press-activated - ::slotted(.mdc-list-item--activated.mdc-list-item:not(.mdc-list-item--disabled):not(.mdc-ripple-upgraded):active) { + ::slotted(.mdc-list-item--activated[mwc-list-item]:not([disabled]):not(.mdc-ripple-upgraded):active) { &::after { // Opacity falls under color because the chosen opacity is color-dependent in typical usage @include mdc-feature-targets($feat-color) { @@ -376,9 +341,14 @@ $dense-item-primary-text-baseline-height: 24px; } } - ::slotted(.mdc-list-item--activated.mdc-list-item:not(.mdc-list-item--disabled).mdc-ripple-upgraded) { + ::slotted(.mdc-list-item--activated[mwc-list-item]:not([disabled]).mdc-ripple-upgraded) { @include mdc-feature-targets($feat-color) { --mdc-ripple-fg-opacity: #{$press-opacity-primary + $activated-opacity}; } } +} + +.mdc-list--dense ::slotted(.mdc-list-item__primary-text) { + @include mdc-typography-baseline-top($dense-item-primary-text-baseline-height, $query); + @include mdc-typography-baseline-bottom($item-secondary-text-baseline-height, $query); } \ No newline at end of file From 56981f618e2ee79b9c19a454fc4ae2b4d94001bb Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Thu, 9 Jan 2020 19:57:11 -0800 Subject: [PATCH 027/161] fix list controls --- packages/list/src/mwc-list-base.ts | 91 +++++++++++++++++-------- packages/list/src/mwc-list-item-base.ts | 32 +++++---- packages/list/src/mwc-list-item.ts | 2 +- packages/select/src/mwc-select-base.ts | 2 +- 4 files changed, 81 insertions(+), 46 deletions(-) diff --git a/packages/list/src/mwc-list-base.ts b/packages/list/src/mwc-list-base.ts index b362a2a267..7b17ca65e7 100644 --- a/packages/list/src/mwc-list-base.ts +++ b/packages/list/src/mwc-list-base.ts @@ -20,7 +20,7 @@ import {MDCListAdapter} from '@material/list/adapter'; import MDCListFoundation from '@material/list/foundation.js'; import {MDCListIndex} from '@material/list/types'; import {BaseElement, observer} from '@material/mwc-base/base-element.js'; -import {isNodeElement, doesSlotContainElement} from '@material/mwc-base/utils'; +import {doesSlotContainElement, isNodeElement} from '@material/mwc-base/utils'; import {html, property, query} from 'lit-element'; import {ListItemBase} from './mwc-list-item-base'; @@ -47,29 +47,34 @@ export abstract class ListBase extends BaseElement { if (slot) { return slot.assignedNodes({flatten: true}) - .filter((node) => isNodeElement(node)) as - Element[]; + .filter((node) => isNodeElement(node)) as Element[]; } return []; } + protected items_: ListItemBase[] = []; + get items(): ListItemBase[] { + return this.items_; + } + + protected updateItems() { const nodes = this.assignedElements; const listItems = nodes .map((element) => { - if (element instanceof ListItemBase) { + if (element.hasAttribute('mwc-list-item')) { return element; } - return Array.from(element.querySelectorAll('.list-item')); + return Array.from(element.querySelectorAll('[mwc-list-item]')); }) .reduce((listItems, listItemResult) => { return listItems.concat(listItemResult); }, []); - return listItems as ListItemBase[]; + this.items_ = listItems as ListItemBase[]; } protected selected_: ListItemBase|null = null; @@ -94,7 +99,10 @@ export abstract class ListBase extends BaseElement { @click=${this.listOnClick} @focusin=${this.listOnFocusin} @focusout=${this.listOnFocusout}> - + +
    `; } @@ -122,14 +130,28 @@ export abstract class ListBase extends BaseElement { } } + protected shouldToggleCheckbox(target: (Node&ParentNode)|Element|ListItemBase| + null) { + if (!target || !isNodeElement(target)) { + return false; + } else if ('checked' in target) { + return false; + } else if ((target as Element).hasAttribute('mwc-list-item')) { + const castedTarget = target as ListItemBase; + + return castedTarget.hasRadio || castedTarget.hasCheckbox; + } + + return this.shouldToggleCheckbox(target.parentNode); + } + protected listOnClick(evt: MouseEvent) { if (this.mdcFoundation && this.mdcRoot) { const index = this.getIndexOfTarget(evt); - const target = evt.target as Element | null; - const toggleCheckbox = target && 'getAttribute' in target ? - target.getAttribute('role') === 'radio' && - target.getAttribute('aria-checked') === 'true' : - false; + const target = evt.target as ListItemBase | Element | null; + + const toggleCheckbox = this.shouldToggleCheckbox(target); + this.mdcFoundation.handleClick(index, toggleCheckbox); } } @@ -156,8 +178,7 @@ export abstract class ListBase extends BaseElement { return { getListItemCount: () => { if (this.mdcRoot) { - const elements = this.items; - return elements.length; + return this.items.length; } return 0; @@ -167,9 +188,7 @@ export abstract class ListBase extends BaseElement { return -1; } - const elements = this.items; - - if (!elements.length) { + if (!this.items.length) { return -1; } @@ -197,7 +216,7 @@ export abstract class ListBase extends BaseElement { const activeListItem = activeItem as ListItemBase | null; - return activeListItem ? elements.indexOf(activeListItem) : -1; + return activeListItem ? this.items.indexOf(activeListItem) : -1; }, getAttributeForElementIndex: (index, attr) => { const listElement = this.mdcRoot; @@ -254,16 +273,7 @@ export abstract class ListBase extends BaseElement { (element as HTMLElement).focus(); } }, - setTabIndexForListItemChildren: (index, tabIndex) => { - if (!this.mdcRoot) { - return; - } - - const element = this.items[index]; - if (element) { - element.setControlTabIndex(tabIndex); - } - }, + setTabIndexForListItemChildren: () => {}, hasCheckboxAtIndex: (index) => { if (!this.mdcRoot) { return false; @@ -387,7 +397,30 @@ export abstract class ListBase extends BaseElement { firstUpdated() { super.firstUpdated(); - this.mdcFoundation.layout(); this.mdcFoundation.setSingleSelection(!this.multi); } + + onSlotChange() { + this.updateItems(); + this.layout(); + } + + onListItemConnected(e) { + const target = e.target as ListItemBase; + + if (this.items.indexOf(target) === -1) { + this.updateItems(); + } + + this.layout(); + } + + layout() { + this.mdcFoundation.layout(); + const first = this.items[0]; + + if (first) { + first.setAttribute('tabIndex', '0'); + } + } } diff --git a/packages/list/src/mwc-list-item-base.ts b/packages/list/src/mwc-list-item-base.ts index ff2939c99c..fadf67ab4b 100644 --- a/packages/list/src/mwc-list-item-base.ts +++ b/packages/list/src/mwc-list-item-base.ts @@ -16,6 +16,7 @@ */ import {observer} from '@material/mwc-base/observer'; +import {findAssignedElement} from '@material/mwc-base/utils'; import {html, LitElement, property, query} from 'lit-element'; interface HasChecked extends Element { @@ -23,9 +24,9 @@ interface HasChecked extends Element { } export class ListItemBase extends LitElement { @query('slot') protected slotElement!: HTMLSlotElement|null; - @query('label') protected labelElement!: HTMLLabelElement|null; @property({type: String}) value = ''; + @property({type: String}) controlSelector = '.control'; @property({type: Boolean}) hasCheckbox = false; @property({type: Boolean}) hasRadio = false; @property({type: Boolean, reflect: true, attribute: 'disabled'}) @@ -47,25 +48,18 @@ export class ListItemBase extends LitElement { return textContent ? textContent.trim() : ''; } - protected createRenderRoot() { - return this.attachShadow({mode: 'open', delegatesFocus: true}); - } - render() { - return html` - `; + return html``; } protected getControl(): HasChecked|null { - const label = this.labelElement; + const slot = this.slotElement; - if (!label) { + if (!slot) { return null; } - const control = label.control as HTMLElement | HasChecked | null; + const control = findAssignedElement(slot, this.controlSelector); return control && 'checked' in control ? control : null; } @@ -90,15 +84,23 @@ export class ListItemBase extends LitElement { } } - firstUpdated() { + connectedCallback() { + super.connectedCallback(); + this.setAttribute('role', 'option'); if (!this.hasAttribute('tabindex')) { this.setAttribute('tabindex', '-1'); } - this.setControlTabIndex('-1'); - this.toggleAttribute('mwc-list-item', true); } + + firstUpdated() { + if (this.getAttribute('tabindex') === '-1') { + this.setControlTabIndex('-1'); + } + this.dispatchEvent( + new Event('list-item-rendered', {bubbles: true, composed: true})); + } } diff --git a/packages/list/src/mwc-list-item.ts b/packages/list/src/mwc-list-item.ts index d04e61c127..8186b417ef 100644 --- a/packages/list/src/mwc-list-item.ts +++ b/packages/list/src/mwc-list-item.ts @@ -17,8 +17,8 @@ limitations under the License. import {customElement} from 'lit-element'; -import {style} from './mwc-list-item-css.js'; import {ListItemBase} from './mwc-list-item-base.js'; +import {style} from './mwc-list-item-css.js'; declare global { interface HTMLElementTagNameMap { diff --git a/packages/select/src/mwc-select-base.ts b/packages/select/src/mwc-select-base.ts index 13639323a8..101e970cfc 100644 --- a/packages/select/src/mwc-select-base.ts +++ b/packages/select/src/mwc-select-base.ts @@ -25,7 +25,7 @@ import MDCMenuSurfaceFoundation from '@material/menu-surface/foundation.js'; import {MDCMenuAdapter} from '@material/menu/adapter'; import MDCMenuFoundation from '@material/menu/foundation.js'; import {addHasRemoveClass, FormElement, observer} from '@material/mwc-base/form-element.js'; -import {isNodeElement, slotActiveElement, doesSlotContainElement} from '@material/mwc-base/utils'; +import {doesSlotContainElement, isNodeElement, slotActiveElement} from '@material/mwc-base/utils'; import {floatingLabel, FloatingLabel} from '@material/mwc-floating-label'; import {lineRipple, LineRipple} from '@material/mwc-line-ripple'; import {List} from '@material/mwc-list'; From d54bcbc374c22064c4b4f10f26896f341895a4d2 Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Thu, 9 Jan 2020 20:10:09 -0800 Subject: [PATCH 028/161] format --- packages/select/src/mwc-select-base.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/select/src/mwc-select-base.ts b/packages/select/src/mwc-select-base.ts index 101e970cfc..13639323a8 100644 --- a/packages/select/src/mwc-select-base.ts +++ b/packages/select/src/mwc-select-base.ts @@ -25,7 +25,7 @@ import MDCMenuSurfaceFoundation from '@material/menu-surface/foundation.js'; import {MDCMenuAdapter} from '@material/menu/adapter'; import MDCMenuFoundation from '@material/menu/foundation.js'; import {addHasRemoveClass, FormElement, observer} from '@material/mwc-base/form-element.js'; -import {doesSlotContainElement, isNodeElement, slotActiveElement} from '@material/mwc-base/utils'; +import {isNodeElement, slotActiveElement, doesSlotContainElement} from '@material/mwc-base/utils'; import {floatingLabel, FloatingLabel} from '@material/mwc-floating-label'; import {lineRipple, LineRipple} from '@material/mwc-line-ripple'; import {List} from '@material/mwc-list'; From 6310fded474a789919ab10007bb6f9d94b896350 Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Fri, 10 Jan 2020 12:28:09 -0800 Subject: [PATCH 029/161] setup package for menu --- packages/menu/package.json | 24 ++++++++++++++++++++++++ packages/menu/tsconfig.json | 21 +++++++++++++++++++++ packages/select/package.json | 2 +- packages/select/tsconfig.json | 3 +++ 4 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 packages/menu/package.json create mode 100644 packages/menu/tsconfig.json diff --git a/packages/menu/package.json b/packages/menu/package.json new file mode 100644 index 0000000000..89005c739d --- /dev/null +++ b/packages/menu/package.json @@ -0,0 +1,24 @@ +{ + "name": "@material/mwc-menu", + "version": "0.12.0", + "description": "", + "main": "mwc-menu.js", + "module": "mwc-menu.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "Apache-2.0", + "dependencies": { + "@material/mwc-base": "^0.12.0", + "@material/menu": "=5.0.0-canary.5ffe8f7e3.0", + "@material/menu-surface": "=5.0.0-canary.5ffe8f7e3.0", + "@material/mwc-list": "^0.12.0", + "lit-element": "^2.2.1", + "lit-html": "^1.1.2", + "tslib": "^1.10.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/menu/tsconfig.json b/packages/menu/tsconfig.json new file mode 100644 index 0000000000..f4d3f311a9 --- /dev/null +++ b/packages/menu/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "." + }, + "include": [ + "src/**/*.ts" + ], + "references": [ + { + "path": "../base" + }, + { + "path": "../list" + } + ], + "exclude": [ + "src/test/*.ts" + ] +} \ No newline at end of file diff --git a/packages/select/package.json b/packages/select/package.json index a7d16cc481..3429ffe4a5 100644 --- a/packages/select/package.json +++ b/packages/select/package.json @@ -17,12 +17,12 @@ "@material/mwc-floating-label": "^0.12.0", "@material/mwc-icon": "^0.12.0", "@material/mwc-list": "^0.12.0", + "@material/mwc-menu": "^0.12.0", "@material/mwc-line-ripple": "^0.12.0", "@material/mwc-notched-outline": "^0.12.0", "@material/select": "=5.0.0-canary.5ffe8f7e3.0", "@material/menu": "=5.0.0-canary.5ffe8f7e3.0", "@material/menu-surface": "=5.0.0-canary.5ffe8f7e3.0", - "@material/list": "=5.0.0-canary.5ffe8f7e3.0", "lit-element": "^2.2.1", "lit-html": "^1.1.2", "tslib": "^1.10.0" diff --git a/packages/select/tsconfig.json b/packages/select/tsconfig.json index 7f0d308228..3f3a04d652 100644 --- a/packages/select/tsconfig.json +++ b/packages/select/tsconfig.json @@ -22,6 +22,9 @@ { "path": "../list" }, + { + "path": "../menu" + }, { "path": "../notched-outline" }, From 38ae407ceb90d4ca921d26ff3433314f44239ed1 Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Fri, 10 Jan 2020 17:42:02 -0800 Subject: [PATCH 030/161] menu-surface implementation --- packages/base/src/utils.ts | 30 +- packages/list/src/mwc-list-base.ts | 41 +- packages/list/tsconfig.json | 3 +- packages/menu/src/mwc-menu-base.ts | 0 .../src/mwc-menu-surface-anchor-directive.ts | 45 +++ packages/menu/src/mwc-menu-surface-base.ts | 355 ++++++++++++++++++ packages/menu/src/mwc-menu-surface.scss | 21 ++ packages/menu/src/mwc-menu-surface.ts | 31 ++ packages/menu/src/mwc-menu.scss | 0 packages/menu/src/mwc-menu.ts | 0 packages/menu/tsconfig.json | 3 +- packages/select/src/mwc-select-base.ts | 10 +- packages/textfield/tsconfig.json | 3 +- 13 files changed, 498 insertions(+), 44 deletions(-) create mode 100644 packages/menu/src/mwc-menu-base.ts create mode 100644 packages/menu/src/mwc-menu-surface-anchor-directive.ts create mode 100644 packages/menu/src/mwc-menu-surface-base.ts create mode 100644 packages/menu/src/mwc-menu-surface.scss create mode 100644 packages/menu/src/mwc-menu-surface.ts create mode 100644 packages/menu/src/mwc-menu.scss create mode 100644 packages/menu/src/mwc-menu.ts diff --git a/packages/base/src/utils.ts b/packages/base/src/utils.ts index a6f53a3d63..d639942df1 100644 --- a/packages/base/src/utils.ts +++ b/packages/base/src/utils.ts @@ -73,7 +73,7 @@ document.removeEventListener('x', fn); */ export const supportsPassiveEventListener = supportsPassive; -export const slotActiveElement = (slot: HTMLSlotElement): Element|null => { +const slotActiveElement = (slot: HTMLSlotElement): Element|null => { const assignedElements = slot.assignedNodes({flatten: true}) .filter((node) => isNodeElement(node)) as Element[]; @@ -88,7 +88,7 @@ export const slotActiveElement = (slot: HTMLSlotElement): Element|null => { }; export const doesSlotContainElement = - (slot: HTMLSlotElement, element: Element) => { + (slot: HTMLSlotElement, element: Element): boolean => { return slot.assignedNodes({flatten: true}) .filter((node) => isNodeElement(node)) .reduce((isContained: boolean, assinedElement) => { @@ -96,3 +96,29 @@ export const doesSlotContainElement = assinedElement.contains(element); }, false); }; + +export const doesSlotContainFocus = (slot: HTMLSlotElement): boolean => { + const activeElement = slotActiveElement(slot); + + return activeElement ? doesSlotContainElement(slot, activeElement) : false; +}; + +export const deepActiveElementPath = (doc = window.document): Element[] => { + let activeElement = doc.activeElement; + const path: Element[] = []; + + if (!activeElement) { + return path; + } + + while (activeElement) { + path.push(activeElement); + if (activeElement.shadowRoot) { + activeElement = activeElement.shadowRoot.activeElement; + } else { + break; + } + } + + return path; +}; diff --git a/packages/list/src/mwc-list-base.ts b/packages/list/src/mwc-list-base.ts index 7b17ca65e7..123bfb0852 100644 --- a/packages/list/src/mwc-list-base.ts +++ b/packages/list/src/mwc-list-base.ts @@ -20,7 +20,7 @@ import {MDCListAdapter} from '@material/list/adapter'; import MDCListFoundation from '@material/list/foundation.js'; import {MDCListIndex} from '@material/list/types'; import {BaseElement, observer} from '@material/mwc-base/base-element.js'; -import {doesSlotContainElement, isNodeElement} from '@material/mwc-base/utils'; +import {deepActiveElementPath, doesSlotContainFocus, isNodeElement} from '@material/mwc-base/utils'; import {html, property, query} from 'lit-element'; import {ListItemBase} from './mwc-list-item-base'; @@ -192,31 +192,21 @@ export abstract class ListBase extends BaseElement { return -1; } - const activeElement = this.getSlottedActiveElement(); + const activeElementPath = deepActiveElementPath(); - if (!activeElement) { + if (!activeElementPath.length) { return -1; } - let activeItem: ListItemBase|Element|null = activeElement; + for (let i = activeElementPath.length - 1; i >= 0; i--) { + const activeItem = activeElementPath[i]; - while (activeItem && !(activeItem instanceof ListItemBase)) { - const parent = activeItem.parentNode; - if (!parent) { - activeItem = null; - continue; - } - - if (parent.nodeType === Node.ELEMENT_NODE) { - activeItem = parent as Element; - } else { - activeItem = null; + if (activeItem.hasAttribute('mwc-list-item')) { + return this.items.indexOf(activeItem as ListItemBase); } } - const activeListItem = activeItem as ListItemBase | null; - - return activeListItem ? this.items.indexOf(activeListItem) : -1; + return -1; }, getAttributeForElementIndex: (index, attr) => { const listElement = this.mdcRoot; @@ -351,24 +341,13 @@ export abstract class ListBase extends BaseElement { }; } - protected getSlottedActiveElement(): Element|null { - const first = this.items[0]; - if (!first) { - return null; - } - - const root = first.getRootNode() as unknown as DocumentOrShadowRoot; - return root ? root.activeElement : null; - } - doContentsHaveFocus(): boolean { const slotElement = this.slotElement; - const activeElement = this.getSlottedActiveElement(); - if (!activeElement || !slotElement) { + if (!slotElement) { return false; } - return doesSlotContainElement(slotElement, activeElement); + return doesSlotContainFocus(slotElement); } select(index: number) { diff --git a/packages/list/tsconfig.json b/packages/list/tsconfig.json index 5b21d43305..dc9aaeee45 100644 --- a/packages/list/tsconfig.json +++ b/packages/list/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../../tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "." + "outDir": ".", + "tsBuildInfoFile": ".tsbuildinfo" }, "include": [ "src/**/*.ts" diff --git a/packages/menu/src/mwc-menu-base.ts b/packages/menu/src/mwc-menu-base.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/menu/src/mwc-menu-surface-anchor-directive.ts b/packages/menu/src/mwc-menu-surface-anchor-directive.ts new file mode 100644 index 0000000000..872c7514df --- /dev/null +++ b/packages/menu/src/mwc-menu-surface-anchor-directive.ts @@ -0,0 +1,45 @@ +/** +@license +Copyright 2020 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import {directive, PropertyPart} from 'lit-html'; +import {AnchorableElement} from './mwc-menu-surface-base'; + +export interface MenuAnchor extends HTMLElement { + anchoring: Element|null; +} + +const partToTarget = new WeakMap(); + +export const menuAnchor = + directive((forSelector: string) => (part: PropertyPart) => { + const lastTarget = partToTarget.get(part); + const anchorElement = part.committer.element; + const root = anchorElement.getRootNode() as Document | ShadowRoot; + const target = + root.querySelector(forSelector) as AnchorableElement | null; + + if (target !== lastTarget) { + anchorElement.classList.add('mdc-menu-anchor'); + + if (target) { + target.anchorElement = anchorElement; + partToTarget.set(part, target); + } + + partToTarget.set(part, target); + part.setValue(target); + } + }); diff --git a/packages/menu/src/mwc-menu-surface-base.ts b/packages/menu/src/mwc-menu-surface-base.ts new file mode 100644 index 0000000000..4baf17218a --- /dev/null +++ b/packages/menu/src/mwc-menu-surface-base.ts @@ -0,0 +1,355 @@ +/** +@license +Copyright 2020 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import {MDCMenuSurfaceAdapter} from '@material/menu-surface/adapter'; +import MDCMenuSurfaceFoundation from '@material/menu-surface/foundation.js'; +import {getTransformPropertyName} from '@material/menu-surface/util'; +import {addHasRemoveClass, BaseElement, observer} from '@material/mwc-base/base-element.js'; +import {deepActiveElementPath, doesSlotContainElement, doesSlotContainFocus, isNodeElement} from '@material/mwc-base/utils'; +import {html, query, property} from 'lit-element'; +import {classMap} from 'lit-html/directives/class-map'; + +export type AnchorableElement = HTMLElement&{anchorElement: Element | null}; + +export abstract class MenuSurfaceBase extends BaseElement { + protected mdcFoundation!: MDCMenuSurfaceFoundation; + + protected readonly mdcFoundationClass = MDCMenuSurfaceFoundation; + + @query('.mdc-menu-surface') mdcRoot!: HTMLDivElement; + + @query('slot') slotElement!: HTMLSlotElement; + + @property({type: Boolean}) + @observer(function(this: MenuSurfaceBase, isAbsolute: boolean) { + if (this.mdcFoundation && !this.fixed) { + this.mdcFoundation.setIsHoisted(isAbsolute); + + this.saveOrRestoreAnchor(isAbsolute); + } + }) + absolute = false; + + @property({type: Object}) + @observer(function(this: MenuSurfaceBase, newAnchor: HTMLElement|null, oldAnchor: HTMLElement |null) { + if (oldAnchor) { + oldAnchor.style.position = ''; + oldAnchor.style.overflow = ''; + } + if (newAnchor) { + newAnchor.style.position = 'relative'; + newAnchor.style.overflow = 'visible'; + } + }) + anchorElement: HTMLElement|null = null; + + @property({type: Boolean}) + @observer(function(this: MenuSurfaceBase, isFixed: boolean) { + if (this.mdcFoundation && !this.absolute) { + this.mdcFoundation.setIsHoisted(isFixed); + this.saveOrRestoreAnchor(isFixed); + } + }) + fixed = false; + + @property({type: Number}) + @observer(function(this: MenuSurfaceBase, value: number|null) { + if (this.mdcFoundation && this.y !== null && value !== null) { + this.mdcFoundation.setAbsolutePosition(value, this.y); + } + }) + x: number | null = null; + + @property({type: Number}) + @observer(function(this: MenuSurfaceBase, value: number|null) { + if (this.mdcFoundation && this.x !== null && value !== null) { + this.mdcFoundation.setAbsolutePosition(this.x, value); + } + }) + y: number | null = null; + + @property({type: Boolean, reflect: true}) + @observer(function(this: MenuSurfaceBase, isOpen: boolean) { + if (this.mdcFoundation) { + if (isOpen) { + this.mdcFoundation.open(); + } else { + this.mdcFoundation.close(); + } + } + }) + open = false; + + @property({type: Boolean}) + @observer(function(this: MenuSurfaceBase, value: boolean) { + if (this.mdcFoundation) { + this.mdcFoundation.setQuickOpen(value); + } + }) + quick = false; + + protected previouslyFocused: HTMLElement|Element|null = null; + protected previousAnchor: HTMLElement|null = null; + protected onBodyClickBound: (evt: MouseEvent) => void = () => {}; + + render() { + const classes = { + 'mdc-menu-surface--fixed': this.fixed, + }; + + return html` +
    + +
    `; + } + + createAdapter(): MDCMenuSurfaceAdapter { + return { + ...addHasRemoveClass(this.mdcRoot), + hasAnchor: () => { + if (!this.mdcRoot) { + return false; + } + + return !!this.anchorElement; + }, + notifyClose: () => { + if (!this.mdcRoot) { + return; + } + + const init: CustomEventInit = {}; + const ev = new CustomEvent('closed', init); + this.open = false; + this.mdcRoot.dispatchEvent(ev); + }, + notifyOpen: () => { + if (!this.mdcRoot) { + return; + } + + const init: CustomEventInit = {}; + const ev = new CustomEvent('opened', init); + this.open = true; + this.mdcRoot.dispatchEvent(ev); + }, + isElementInContainer: (element) => { + const root = this.mdcRoot; + const slotElement = this.slotElement; + if (!root || !slotElement) { + return false; + } + + return element === this || element === root || + doesSlotContainElement(slotElement, element); + }, + isRtl: () => { + if (this.mdcRoot) { + return getComputedStyle(this.mdcRoot).direction === 'rtl'; + } + + return false; + }, + setTransformOrigin: (origin) => { + const root = this.mdcRoot; + if (!root) { + return; + } + + const propertyName = `${getTransformPropertyName(window)}-origin`; + root.style.setProperty(propertyName, origin); + }, + isFocused: () => { + const root = this.mdcRoot; + const slotElement = this.slotElement; + if (!root || !slotElement) { + return false; + } + + const docRoot = root.getRootNode() as Document | ShadowRoot; + const elementIsFocused = docRoot.activeElement === root; + + if (elementIsFocused) { + return true; + } + + return doesSlotContainFocus(slotElement); + }, + saveFocus: () => { + const mdcRoot = this.mdcRoot; + + if (!mdcRoot) { + return; + } + + const activeElementPath = deepActiveElementPath(); + const pathLength = activeElementPath.length; + + if (!pathLength) { + this.previouslyFocused = null; + } + + this.previouslyFocused = activeElementPath[pathLength - 1]; + }, + restoreFocus: () => { + const mdcRoot = this.mdcRoot; + const slotElement = this.slotElement; + + if (!slotElement || !mdcRoot) { + return; + } + const menuHasFocus = doesSlotContainFocus(slotElement); + + if (!menuHasFocus) { + return; + } + + if (!this.previouslyFocused) { + return; + } + + if ('focus' in this.previouslyFocused) { + this.previouslyFocused.focus(); + } + }, + getInnerDimensions: () => { + const mdcRoot = this.mdcRoot; + + if (!mdcRoot) { + return {width: 0, height: 0}; + } + + return {width: mdcRoot.offsetWidth, height: mdcRoot.offsetHeight}; + }, + getAnchorDimensions: () => { + const anchorElement = this.anchorElement; + + return anchorElement ? anchorElement.getBoundingClientRect() : null; + }, + getBodyDimensions: () => { + return { + width: document.body.clientWidth, + height: document.body.clientHeight, + }; + }, + getWindowDimensions: () => { + return { + width: window.innerWidth, + height: window.innerHeight, + }; + }, + getWindowScroll: () => { + return { + x: window.pageXOffset, + y: window.pageYOffset, + }; + }, + setPosition: (position) => { + const mdcRoot = this.mdcRoot; + + if (!mdcRoot) { + return; + } + + mdcRoot.style.left = 'left' in position ? `${position.left}px` : ''; + mdcRoot.style.right = 'right' in position ? `${position.right}px` : ''; + mdcRoot.style.top = 'top' in position ? `${position.top}px` : ''; + mdcRoot.style.bottom = + 'bottom' in position ? `${position.bottom}px` : ''; + }, + setMaxHeight: (height) => { + const mdcRoot = this.mdcRoot; + + if (!mdcRoot) { + return; + } + + mdcRoot.style.maxHeight = height; + }, + }; + } + + protected onKeydown(evt: KeyboardEvent) { + if (this.mdcFoundation) { + this.mdcFoundation.handleKeydown(evt); + } + } + + protected onBodyClick(evt: MouseEvent) { + if (this.mdcFoundation) { + this.mdcFoundation.handleBodyClick(evt); + } + } + + protected registerBodyClick() { + this.onBodyClickBound = this.onBodyClick.bind(this); + document.body.addEventListener('click', this.onBodyClickBound); + } + + protected deregisterBodyClick() { + document.body.removeEventListener('click', this.onBodyClickBound); + } + + protected getDefaultAnchor(): HTMLElement | null { + const defaultAnchor = this.parentNode; + + if (defaultAnchor) { + if (isNodeElement(defaultAnchor)) { + return defaultAnchor as HTMLElement; + } else if (defaultAnchor.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { + const anchorCasted = defaultAnchor as DocumentFragment | ShadowRoot; + if ('host' in anchorCasted) { + return anchorCasted.host as HTMLElement; + } + } + } + + return null; + } + + protected saveOrRestoreAnchor(isAbsolute: boolean) { + if (isAbsolute) { + this.previousAnchor = this.anchorElement; + this.anchorElement = null; + } + + if (!isAbsolute && !this.anchorElement && !this.previousAnchor) { + this.anchorElement = this.getDefaultAnchor(); + } else if (!isAbsolute && !this.anchorElement && this.previousAnchor) { + this.anchorElement = this.previousAnchor; + } + } + + firstUpdated() { + super.firstUpdated(); + + if (!this.anchorElement && !this.absolute) { + this.anchorElement = this.getDefaultAnchor(); + } + } + + close() { + this.open = false; + } + + show() { + this.open = true; + } +} diff --git a/packages/menu/src/mwc-menu-surface.scss b/packages/menu/src/mwc-menu-surface.scss new file mode 100644 index 0000000000..e48b5789d6 --- /dev/null +++ b/packages/menu/src/mwc-menu-surface.scss @@ -0,0 +1,21 @@ +/** +@license +Copyright 2020 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +@import "@material/menu-surface/mdc-menu-surface.scss"; + +:host(:not([open])) { + display: none; +} diff --git a/packages/menu/src/mwc-menu-surface.ts b/packages/menu/src/mwc-menu-surface.ts new file mode 100644 index 0000000000..c6ff9e9314 --- /dev/null +++ b/packages/menu/src/mwc-menu-surface.ts @@ -0,0 +1,31 @@ +/** +@license +Copyright 2020 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {customElement} from 'lit-element'; +import {MenuSurfaceBase} from './mwc-menu-surface-base.js'; +import {style} from './mwc-menu-surface-css.js'; + +declare global { + interface HTMLElementTagNameMap { + 'mwc-menu-surface': MenuSurface; + } +} + +@customElement('mwc-menu-surface') +export class MenuSurface extends MenuSurfaceBase { + static styles = style; +} diff --git a/packages/menu/src/mwc-menu.scss b/packages/menu/src/mwc-menu.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/menu/src/mwc-menu.ts b/packages/menu/src/mwc-menu.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/menu/tsconfig.json b/packages/menu/tsconfig.json index f4d3f311a9..c380171025 100644 --- a/packages/menu/tsconfig.json +++ b/packages/menu/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../../tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "." + "outDir": ".", + "tsBuildInfoFile": ".tsbuildinfo" }, "include": [ "src/**/*.ts" diff --git a/packages/select/src/mwc-select-base.ts b/packages/select/src/mwc-select-base.ts index 13639323a8..f68105c7cc 100644 --- a/packages/select/src/mwc-select-base.ts +++ b/packages/select/src/mwc-select-base.ts @@ -25,7 +25,7 @@ import MDCMenuSurfaceFoundation from '@material/menu-surface/foundation.js'; import {MDCMenuAdapter} from '@material/menu/adapter'; import MDCMenuFoundation from '@material/menu/foundation.js'; import {addHasRemoveClass, FormElement, observer} from '@material/mwc-base/form-element.js'; -import {isNodeElement, slotActiveElement, doesSlotContainElement} from '@material/mwc-base/utils'; +import {doesSlotContainFocus, isNodeElement} from '@material/mwc-base/utils'; import {floatingLabel, FloatingLabel} from '@material/mwc-floating-label'; import {lineRipple, LineRipple} from '@material/mwc-line-ripple'; import {List} from '@material/mwc-list'; @@ -692,13 +692,7 @@ export abstract class SelectBase extends FormElement { return; } - const activeElement = slotActiveElement(slotElement); - - if (!activeElement) { - return; - } - - const menuHasFocus = doesSlotContainElement(slotElement, activeElement); + const menuHasFocus = doesSlotContainFocus(slotElement); if (!menuHasFocus) { return; diff --git a/packages/textfield/tsconfig.json b/packages/textfield/tsconfig.json index ae59864606..e52ec789e4 100644 --- a/packages/textfield/tsconfig.json +++ b/packages/textfield/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../../tsconfig.json", "compilerOptions": { "rootDir": "src", - "outDir": "." + "outDir": ".", + "tsBuildInfoFile": ".tsbuildinfo" }, "include": [ "src/**/*.ts" From 69a23ad1e4c44acb6a624675367e5579a35eee6f Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Fri, 10 Jan 2020 17:47:52 -0800 Subject: [PATCH 031/161] format + corner --- packages/menu/src/mwc-menu-surface-base.ts | 35 ++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/menu/src/mwc-menu-surface-base.ts b/packages/menu/src/mwc-menu-surface-base.ts index 4baf17218a..fca75f25db 100644 --- a/packages/menu/src/mwc-menu-surface-base.ts +++ b/packages/menu/src/mwc-menu-surface-base.ts @@ -19,9 +19,32 @@ import MDCMenuSurfaceFoundation from '@material/menu-surface/foundation.js'; import {getTransformPropertyName} from '@material/menu-surface/util'; import {addHasRemoveClass, BaseElement, observer} from '@material/mwc-base/base-element.js'; import {deepActiveElementPath, doesSlotContainElement, doesSlotContainFocus, isNodeElement} from '@material/mwc-base/utils'; -import {html, query, property} from 'lit-element'; +import {html, property, query} from 'lit-element'; import {classMap} from 'lit-html/directives/class-map'; +enum CornerBit { + BOTTOM = 1, + CENTER = 2, + RIGHT = 4, + FLIP_RTL = 8, +} + +export enum Corner { + TOP_LEFT = 0, + TOP_RIGHT = CornerBit.RIGHT, + BOTTOM_LEFT = CornerBit.BOTTOM, + BOTTOM_RIGHT = + CornerBit.BOTTOM | CornerBit.RIGHT, // tslint:disable-line:no-bitwise + TOP_START = CornerBit.FLIP_RTL, + TOP_END = + CornerBit.FLIP_RTL | CornerBit.RIGHT, // tslint:disable-line:no-bitwise + BOTTOM_START = + CornerBit.BOTTOM | CornerBit.FLIP_RTL, // tslint:disable-line:no-bitwise + BOTTOM_END = CornerBit.BOTTOM | CornerBit.RIGHT | + CornerBit.FLIP_RTL, // tslint:disable-line:no-bitwise +} +; + export type AnchorableElement = HTMLElement&{anchorElement: Element | null}; export abstract class MenuSurfaceBase extends BaseElement { @@ -44,7 +67,9 @@ export abstract class MenuSurfaceBase extends BaseElement { absolute = false; @property({type: Object}) - @observer(function(this: MenuSurfaceBase, newAnchor: HTMLElement|null, oldAnchor: HTMLElement |null) { + @observer(function( + this: MenuSurfaceBase, newAnchor: HTMLElement|null, + oldAnchor: HTMLElement|null) { if (oldAnchor) { oldAnchor.style.position = ''; oldAnchor.style.overflow = ''; @@ -71,7 +96,7 @@ export abstract class MenuSurfaceBase extends BaseElement { this.mdcFoundation.setAbsolutePosition(value, this.y); } }) - x: number | null = null; + x: number|null = null; @property({type: Number}) @observer(function(this: MenuSurfaceBase, value: number|null) { @@ -79,7 +104,7 @@ export abstract class MenuSurfaceBase extends BaseElement { this.mdcFoundation.setAbsolutePosition(this.x, value); } }) - y: number | null = null; + y: number|null = null; @property({type: Boolean, reflect: true}) @observer(function(this: MenuSurfaceBase, isOpen: boolean) { @@ -307,7 +332,7 @@ export abstract class MenuSurfaceBase extends BaseElement { document.body.removeEventListener('click', this.onBodyClickBound); } - protected getDefaultAnchor(): HTMLElement | null { + protected getDefaultAnchor(): HTMLElement|null { const defaultAnchor = this.parentNode; if (defaultAnchor) { From 29392aeea459ac590f18a3e7cecf69cf8fdc8203 Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Fri, 10 Jan 2020 18:05:23 -0800 Subject: [PATCH 032/161] add missing fns to menu-surface --- packages/menu/src/mwc-menu-surface-base.ts | 45 +++++++++++----------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/packages/menu/src/mwc-menu-surface-base.ts b/packages/menu/src/mwc-menu-surface-base.ts index fca75f25db..67dd9349bb 100644 --- a/packages/menu/src/mwc-menu-surface-base.ts +++ b/packages/menu/src/mwc-menu-surface-base.ts @@ -21,30 +21,11 @@ import {addHasRemoveClass, BaseElement, observer} from '@material/mwc-base/base- import {deepActiveElementPath, doesSlotContainElement, doesSlotContainFocus, isNodeElement} from '@material/mwc-base/utils'; import {html, property, query} from 'lit-element'; import {classMap} from 'lit-html/directives/class-map'; +import {Corner as CornerEnum} from '@material/menu-surface/constants'; +import {MDCMenuDistance} from '@material/menu-surface/types'; -enum CornerBit { - BOTTOM = 1, - CENTER = 2, - RIGHT = 4, - FLIP_RTL = 8, -} - -export enum Corner { - TOP_LEFT = 0, - TOP_RIGHT = CornerBit.RIGHT, - BOTTOM_LEFT = CornerBit.BOTTOM, - BOTTOM_RIGHT = - CornerBit.BOTTOM | CornerBit.RIGHT, // tslint:disable-line:no-bitwise - TOP_START = CornerBit.FLIP_RTL, - TOP_END = - CornerBit.FLIP_RTL | CornerBit.RIGHT, // tslint:disable-line:no-bitwise - BOTTOM_START = - CornerBit.BOTTOM | CornerBit.FLIP_RTL, // tslint:disable-line:no-bitwise - BOTTOM_END = CornerBit.BOTTOM | CornerBit.RIGHT | - CornerBit.FLIP_RTL, // tslint:disable-line:no-bitwise -} -; - +export {MDCMenuDistance} from '@material/menu-surface/types'; +export type Corner = keyof typeof CornerEnum; export type AnchorableElement = HTMLElement&{anchorElement: Element | null}; export abstract class MenuSurfaceBase extends BaseElement { @@ -126,6 +107,18 @@ export abstract class MenuSurfaceBase extends BaseElement { }) quick = false; + @property({type: String}) + @observer(function(this: MenuSurfaceBase, value: Corner|null) { + if (this.mdcFoundation) { + if (value) { + this.mdcFoundation.setAnchorCorner(CornerEnum[value]); + } else { + this.mdcFoundation.setAnchorCorner(CornerEnum.TOP_START); + } + } + }) + corner: Corner | null = null; + protected previouslyFocused: HTMLElement|Element|null = null; protected previousAnchor: HTMLElement|null = null; protected onBodyClickBound: (evt: MouseEvent) => void = () => {}; @@ -377,4 +370,10 @@ export abstract class MenuSurfaceBase extends BaseElement { show() { this.open = true; } + + setAnchorMargin(margin: MDCMenuDistance) { + if (this.mdcFoundation) { + this.mdcFoundation.setAnchorMargin(margin); + } + } } From 5508bd934217bcd1dd48ca5aac8445fed52c7178 Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Fri, 10 Jan 2020 18:06:39 -0800 Subject: [PATCH 033/161] reexport types from element declaration --- packages/menu/src/mwc-menu-surface.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/menu/src/mwc-menu-surface.ts b/packages/menu/src/mwc-menu-surface.ts index c6ff9e9314..f65661fbc8 100644 --- a/packages/menu/src/mwc-menu-surface.ts +++ b/packages/menu/src/mwc-menu-surface.ts @@ -19,6 +19,9 @@ import {customElement} from 'lit-element'; import {MenuSurfaceBase} from './mwc-menu-surface-base.js'; import {style} from './mwc-menu-surface-css.js'; +export {MDCMenuDistance} from '@material/menu-surface/types'; +export {Corner} from './mwc-menu-surface-base'; + declare global { interface HTMLElementTagNameMap { 'mwc-menu-surface': MenuSurface; From 3f7d8c62e45ed3d16080296dc2b499bf9b46e30a Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Fri, 10 Jan 2020 21:26:48 -0800 Subject: [PATCH 034/161] implement menu as element --- packages/list/src/mwc-list-base.ts | 23 +- packages/list/src/mwc-list-item-base.ts | 2 - packages/list/src/mwc-list-item.scss | 25 ++ packages/menu/src/mwc-menu-base.ts | 327 ++++++++++++++++++ .../src/mwc-menu-surface-anchor-directive.ts | 2 +- packages/menu/src/mwc-menu-surface-base.ts | 35 +- packages/menu/src/mwc-menu.ts | 34 ++ packages/select/src/mwc-select-base.ts | 2 +- 8 files changed, 425 insertions(+), 25 deletions(-) diff --git a/packages/list/src/mwc-list-base.ts b/packages/list/src/mwc-list-base.ts index 123bfb0852..b806a4b451 100644 --- a/packages/list/src/mwc-list-base.ts +++ b/packages/list/src/mwc-list-base.ts @@ -42,6 +42,16 @@ export abstract class ListBase extends BaseElement { }) multi = false; + @property({type: Boolean}) + @observer(function(this: ListBase, value: boolean) { + if (this.mdcFoundation) { + this.mdcFoundation.setWrapFocus(!value); + } + }) + wrapFocus = false; + + @property({type: String}) itemRoles: string|null = null; + protected get assignedElements(): Element[] { const slot = this.slotElement; @@ -75,6 +85,13 @@ export abstract class ListBase extends BaseElement { }, []); this.items_ = listItems as ListItemBase[]; + this.items_.forEach((item) => { + if (this.itemRoles) { + item.setAttribute('role', this.itemRoles); + } else { + item.removeAttribute('role'); + } + }); } protected selected_: ListItemBase|null = null; @@ -367,12 +384,6 @@ export abstract class ListBase extends BaseElement { this.selected_ = itemToSelect; } - wrapFocus(wrapFocus: boolean) { - if (this.mdcFoundation) { - this.mdcFoundation.setWrapFocus(wrapFocus); - } - } - firstUpdated() { super.firstUpdated(); diff --git a/packages/list/src/mwc-list-item-base.ts b/packages/list/src/mwc-list-item-base.ts index fadf67ab4b..042c4a8b2c 100644 --- a/packages/list/src/mwc-list-item-base.ts +++ b/packages/list/src/mwc-list-item-base.ts @@ -87,8 +87,6 @@ export class ListItemBase extends LitElement { connectedCallback() { super.connectedCallback(); - this.setAttribute('role', 'option'); - if (!this.hasAttribute('tabindex')) { this.setAttribute('tabindex', '-1'); } diff --git a/packages/list/src/mwc-list-item.scss b/packages/list/src/mwc-list-item.scss index 8dab3b8fbf..00be9f1112 100644 --- a/packages/list/src/mwc-list-item.scss +++ b/packages/list/src/mwc-list-item.scss @@ -140,3 +140,28 @@ $dense-item-primary-text-baseline-height: 24px; } } +:host([toggleIcon]:not([selected])) .mdc-list-item__graphic { + display: none; +} + +:host([toggleIcon]) #wrapper { + // postcss-bem-linter: define menu + @include mdc-feature-targets($feat-structure) { + fill: currentColor; + } + + @include mdc-feature-targets($feat-structure) { + @include mdc-rtl-reflexive-property(padding, 56px, $mdc-list-side-padding); + } + + // Extra specificity required to override `display` property on `mdc-list-item__graphic`. + #iconWrapper { + @include mdc-feature-targets($feat-structure) { + @include mdc-rtl-reflexive-position(left, 16px); + position: absolute; + // IE11 requires the icon to be vertically centered due to its absolute positioning + top: 50%; + transform: translateY(-50%); + } + } +} \ No newline at end of file diff --git a/packages/menu/src/mwc-menu-base.ts b/packages/menu/src/mwc-menu-base.ts index e69de29bb2..d79365510f 100644 --- a/packages/menu/src/mwc-menu-base.ts +++ b/packages/menu/src/mwc-menu-base.ts @@ -0,0 +1,327 @@ +/** +@license +Copyright 2020 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import '@material/mwc-list'; +import './mwc-menu-surface'; + +import {Corner, MDCMenuDistance} from './mwc-menu-surface-base'; +import {MDCMenuAdapter} from '@material/menu/adapter'; +import MDCMenuFoundation from '@material/menu/foundation.js'; +import {BaseElement, observer} from '@material/mwc-base/base-element.js'; +import {isNodeElement} from '@material/mwc-base/utils'; +import {List} from '@material/mwc-list'; +import {ListItemBase} from '@material/mwc-list/src/mwc-list-item-base'; +import {html, property, query} from 'lit-element'; +import {MenuSurface} from './mwc-menu-surface'; +import {DefaultFocusState} from '@material/menu/constants'; + +export {Corner} from './mwc-menu-surface-base'; +export {DefaultFocusState} from '@material/menu/constants'; + +/** + * @fires selected + */ +export abstract class MenuBase extends BaseElement { + protected mdcFoundation!: MDCMenuFoundation; + + protected readonly mdcFoundationClass = MDCMenuFoundation; + + @query('.mdc-menu') mdcRoot!: MenuSurface; + + @query('slot') slotElement!: HTMLSlotElement; + + @query('.mdc-list') listElement!: List; + + @property({type: Object}) anchor: HTMLElement|null = null; + + @property({type: Boolean, reflect: true}) open = false; + + @property({type: Boolean}) quick = false; + + @property({type: Boolean}) wrapFocus = false; + + @property({type: String, reflect: true}) role: 'menu'|'listbox' = 'menu'; + + @property({type: String}) corner: Corner|null = null; + + @property({type: Number}) x: number|null = null; + + @property({type: Number}) y: number|null = null; + + @property({type: Boolean}) absolute = false; + + @property({type: Boolean}) fixed = false; + + @property({type: Number}) + @observer(function(this: MenuBase, value: DefaultFocusState) { + if (this.mdcFoundation) { + this.mdcFoundation.setDefaultFocusState(value); + } + }) + defaultFocus: DefaultFocusState = DefaultFocusState.LIST_ROOT; + + get items(): ListItemBase[] { + const listElement = this.listElement; + + if (listElement) { + return listElement.items; + } + + return []; + } + + get selected(): ListItemBase|null { + const listElement = this.listElement; + + if (listElement) { + return listElement.selected; + } + + return null; + } + + render() { + const itemRoles = this.role === 'menu' ? 'menuitem' : 'option'; + + return html` + + + + + `; + } + + createAdapter(): MDCMenuAdapter { + return { + addClassToElementAtIndex: (index, className) => { + const listElement = this.listElement; + if (!listElement) { + return; + } + + const element = listElement.items[index]; + + if (!element) { + return; + } + + element.classList.add(className); + }, + removeClassFromElementAtIndex: (index, className) => { + const listElement = this.listElement; + if (!listElement) { + return; + } + + const element = listElement.items[index]; + + if (!element) { + return; + } + + element.classList.remove(className); + }, + addAttributeToElementAtIndex: (index, attr, value) => { + const listElement = this.listElement; + if (!listElement) { + return; + } + + const element = listElement.items[index]; + + if (!element) { + return; + } + + element.setAttribute(attr, value); + }, + removeAttributeFromElementAtIndex: (index, attr) => { + const listElement = this.listElement; + if (!listElement) { + return; + } + + const element = listElement.items[index]; + + if (!element) { + return; + } + + element.removeAttribute(attr); + }, + elementContainsClass: (element, className) => + element.classList.contains(className), + closeSurface: () => { + this.open = false; + }, + getElementIndex: (element) => { + const listElement = this.listElement; + if (listElement) { + return listElement.items.indexOf(element as ListItemBase); + } + + return -1; + }, + notifySelected: (evtData) => { + if (!this.mdcRoot) { + return; + } + + const init: CustomEventInit = {}; + init.detail = {index: evtData.index, item: evtData}; + const ev = new CustomEvent('selected', init); + this.mdcRoot.dispatchEvent(ev); + }, + getMenuItemCount: () => { + const listElement = this.listElement; + if (!listElement) { + return 0; + } + + return listElement.items.length; + }, + focusItemAtIndex: (index) => { + const listElement = this.listElement; + if (!listElement) { + return; + } + + const element = listElement.items[index]; + + if (element && isNodeElement(element)) { + (element as HTMLElement).focus(); + } + }, + focusListRoot: () => { + if (this.listElement) { + this.listElement.focus(); + } + }, + getSelectedSiblingOfItemAtIndex: (index) => { + const listElement = this.listElement; + + if (!listElement) { + return -1; + } + + const elementAtIndex = listElement.items[index]; + + if (!elementAtIndex) { + return -1; + } + + const groupEl = elementAtIndex.parentNode as HTMLElement; + + if (!isNodeElement(groupEl) && + !groupEl.hasAttribute('mwc-menu-group')) { + return -1; + } + + const selectedItemEl = + groupEl.querySelector('[mwc-list-item][selected]') as ListItemBase | + null; + + if (!selectedItemEl) { + return -1; + } + + const elements = listElement.items; + + return elements.indexOf(selectedItemEl); + }, + isSelectableItemAtIndex: (index) => { + const listElement = this.listElement; + + if (!listElement) { + return false; + } + + const elementAtIndex = listElement.items[index]; + + if (!elementAtIndex) { + return false; + } + + const groupEl = elementAtIndex.parentNode as HTMLElement; + + if (!isNodeElement(groupEl) && + !groupEl.hasAttribute('mwc-menu-group')) { + return false; + } else { + return true; + } + }, + }; + } + + protected onKeydown(evt: KeyboardEvent) { + if (this.mdcFoundation) { + this.mdcFoundation.handleKeydown(evt); + } + } + + protected onAction(evt: CustomEvent<{index: number}>) { + const listElement = this.listElement; + if (this.mdcFoundation && listElement) { + const el = listElement.items[evt.detail.index]; + if (el) { + this.mdcFoundation.handleItemAction(el); + } + } + } + + protected onOpened() { + this.open = true; + + if (this.mdcFoundation) { + this.mdcFoundation.handleMenuSurfaceOpened(); + } + } + + protected onClosed() { + this.open = false; + } + + setAnchorMargin(margin: MDCMenuDistance) { + const surfaceElement = this.mdcRoot; + if (surfaceElement) { + surfaceElement.setAnchorMargin(margin); + } + } + + select(index: number) { + const listElement = this.listElement; + + if (listElement) { + listElement.select(index); + } + } +} diff --git a/packages/menu/src/mwc-menu-surface-anchor-directive.ts b/packages/menu/src/mwc-menu-surface-anchor-directive.ts index 872c7514df..66b9d5e641 100644 --- a/packages/menu/src/mwc-menu-surface-anchor-directive.ts +++ b/packages/menu/src/mwc-menu-surface-anchor-directive.ts @@ -35,7 +35,7 @@ export const menuAnchor = anchorElement.classList.add('mdc-menu-anchor'); if (target) { - target.anchorElement = anchorElement; + target.anchor = anchorElement; partToTarget.set(part, target); } diff --git a/packages/menu/src/mwc-menu-surface-base.ts b/packages/menu/src/mwc-menu-surface-base.ts index 67dd9349bb..22451e7dd5 100644 --- a/packages/menu/src/mwc-menu-surface-base.ts +++ b/packages/menu/src/mwc-menu-surface-base.ts @@ -15,19 +15,24 @@ See the License for the specific language governing permissions and limitations under the License. */ import {MDCMenuSurfaceAdapter} from '@material/menu-surface/adapter'; +import {Corner as CornerEnum} from '@material/menu-surface/constants'; import MDCMenuSurfaceFoundation from '@material/menu-surface/foundation.js'; +import {MDCMenuDistance} from '@material/menu-surface/types'; import {getTransformPropertyName} from '@material/menu-surface/util'; import {addHasRemoveClass, BaseElement, observer} from '@material/mwc-base/base-element.js'; import {deepActiveElementPath, doesSlotContainElement, doesSlotContainFocus, isNodeElement} from '@material/mwc-base/utils'; import {html, property, query} from 'lit-element'; import {classMap} from 'lit-html/directives/class-map'; -import {Corner as CornerEnum} from '@material/menu-surface/constants'; -import {MDCMenuDistance} from '@material/menu-surface/types'; export {MDCMenuDistance} from '@material/menu-surface/types'; + export type Corner = keyof typeof CornerEnum; -export type AnchorableElement = HTMLElement&{anchorElement: Element | null}; +export type AnchorableElement = HTMLElement&{anchor: Element | null}; +/** + * @fires opened + * @fires closed + */ export abstract class MenuSurfaceBase extends BaseElement { protected mdcFoundation!: MDCMenuSurfaceFoundation; @@ -60,7 +65,7 @@ export abstract class MenuSurfaceBase extends BaseElement { newAnchor.style.overflow = 'visible'; } }) - anchorElement: HTMLElement|null = null; + anchor: HTMLElement|null = null; @property({type: Boolean}) @observer(function(this: MenuSurfaceBase, isFixed: boolean) { @@ -117,7 +122,7 @@ export abstract class MenuSurfaceBase extends BaseElement { } } }) - corner: Corner | null = null; + corner: Corner|null = null; protected previouslyFocused: HTMLElement|Element|null = null; protected previousAnchor: HTMLElement|null = null; @@ -146,7 +151,7 @@ export abstract class MenuSurfaceBase extends BaseElement { return false; } - return !!this.anchorElement; + return !!this.anchor; }, notifyClose: () => { if (!this.mdcRoot) { @@ -257,7 +262,7 @@ export abstract class MenuSurfaceBase extends BaseElement { return {width: mdcRoot.offsetWidth, height: mdcRoot.offsetHeight}; }, getAnchorDimensions: () => { - const anchorElement = this.anchorElement; + const anchorElement = this.anchor; return anchorElement ? anchorElement.getBoundingClientRect() : null; }, @@ -344,22 +349,22 @@ export abstract class MenuSurfaceBase extends BaseElement { protected saveOrRestoreAnchor(isAbsolute: boolean) { if (isAbsolute) { - this.previousAnchor = this.anchorElement; - this.anchorElement = null; + this.previousAnchor = this.anchor; + this.anchor = null; } - if (!isAbsolute && !this.anchorElement && !this.previousAnchor) { - this.anchorElement = this.getDefaultAnchor(); - } else if (!isAbsolute && !this.anchorElement && this.previousAnchor) { - this.anchorElement = this.previousAnchor; + if (!isAbsolute && !this.anchor && !this.previousAnchor) { + this.anchor = this.getDefaultAnchor(); + } else if (!isAbsolute && !this.anchor && this.previousAnchor) { + this.anchor = this.previousAnchor; } } firstUpdated() { super.firstUpdated(); - if (!this.anchorElement && !this.absolute) { - this.anchorElement = this.getDefaultAnchor(); + if (!this.anchor && !this.absolute) { + this.anchor = this.getDefaultAnchor(); } } diff --git a/packages/menu/src/mwc-menu.ts b/packages/menu/src/mwc-menu.ts index e69de29bb2..0c81a6812d 100644 --- a/packages/menu/src/mwc-menu.ts +++ b/packages/menu/src/mwc-menu.ts @@ -0,0 +1,34 @@ +/** +@license +Copyright 2020 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {customElement} from 'lit-element'; +import {MenuBase} from './mwc-menu-base.js'; +import {style} from './mwc-menu-css.js'; + +export {Corner} from './mwc-menu-surface-base'; +export {DefaultFocusState} from '@material/menu/constants'; + +declare global { + interface HTMLElementTagNameMap { + 'mwc-menu': MenuBase; + } +} + +@customElement('mwc-menu') +export class Menu extends MenuBase { + static styles = style; +} diff --git a/packages/select/src/mwc-select-base.ts b/packages/select/src/mwc-select-base.ts index f68105c7cc..a10d087da4 100644 --- a/packages/select/src/mwc-select-base.ts +++ b/packages/select/src/mwc-select-base.ts @@ -319,7 +319,7 @@ export abstract class SelectBase extends FormElement { setMenuWrapFocus: (wrapFocus) => { const listElement = this.listElement; if (listElement) { - listElement.wrapFocus(wrapFocus); + listElement.wrapFocus = wrapFocus; } }, setAttributeAtIndex: (index: number, attr: string, value: string) => { From 86ac2af694bcad721f769b582d71174334917b11 Mon Sep 17 00:00:00 2001 From: Elliott Marquez Date: Fri, 10 Jan 2020 22:45:15 -0800 Subject: [PATCH 035/161] pull menu and menu-surface out of select --- packages/list/src/mwc-list-base.ts | 74 +-- packages/list/src/mwc-list.ts | 2 + packages/menu/src/mwc-menu-base.ts | 60 +- packages/menu/src/mwc-menu-surface-base.ts | 24 +- packages/menu/src/mwc-menu.ts | 3 +- packages/select/package.json | 3 - packages/select/src/mwc-menu-ponyfill.ts | 117 ---- .../src/mwc-menu-surface-anchor-directive.ts | 45 -- packages/select/src/mwc-select-base.ts | 575 ++---------------- packages/select/src/mwc-select.scss | 2 - 10 files changed, 146 insertions(+), 759 deletions(-) delete mode 100644 packages/select/src/mwc-menu-ponyfill.ts delete mode 100644 packages/select/src/mwc-menu-surface-anchor-directive.ts diff --git a/packages/list/src/mwc-list-base.ts b/packages/list/src/mwc-list-base.ts index b806a4b451..0ec943c717 100644 --- a/packages/list/src/mwc-list-base.ts +++ b/packages/list/src/mwc-list-base.ts @@ -25,6 +25,8 @@ import {html, property, query} from 'lit-element'; import {ListItemBase} from './mwc-list-item-base'; +export {MDCListIndex} from '@material/list/types'; + export abstract class ListBase extends BaseElement { protected mdcFoundation!: MDCListFoundation; @@ -112,10 +114,10 @@ export abstract class ListBase extends BaseElement { return html`