From 72e8b66b4d2102b200aa6025a6402bdb5f31845f Mon Sep 17 00:00:00 2001 From: "Andrew C. Dvorak" Date: Thu, 21 Feb 2019 12:46:17 -0800 Subject: [PATCH] refactor: Make TS easier to wrap (#4407) ### Goal Make it easier for frameworks to wrap our TypeScript code. Refs #4225 ### How **Isolate framework-specific code from framework-agnostic code.** Specifically, this PR: * Maintains full backward compatibility with existing code * Moves all component definitions from `index.ts` to `component.ts` - `index.ts` is now purely re-exporting other files * Moves component-dependent types from `types.ts` to `component.ts` - Framework-related types are now quarantined in a single file: `component.ts` - All other files are now purely framework-agnostic, and can be safely wrapped by frameworks that don't use our components * Makes `import` paths more specific by switching from `@material/foo/index` to e.g. `@material/foo/foundation` or `@material/foo/types` - The only exception is `component.ts` files: Since they're basically our default "framework", they can safely import _other_ "framework" types via `@material/foo/index` * Updates class & interface export syntax to isolate the `default` export line for future removal if necessary: ```ts export class MDCFooFoundation { // ... } export default MDCFooFoundation; ``` * Combines `typings/custom.d.ts` and `typings/dom.ie.d.ts` into `packages/mdc-dom/externs.d.ts` so the types will be publicly visible * Adds `MDCFooFactory` types for components that need them (e.g., `MDCRipple`, `MDCList`) ### Packages * [x] `animation` * [x] `checkbox` * [x] `chips` * [x] `dialog` * [x] `dom` * [x] `drawer` * [x] `floating-label` * [x] `form-field` * [x] `grid-list` * [x] `icon-button` * [x] `icon-toggle` _(deprecated)_ * [x] `line-ripple` * [x] `linear-progress` * [x] `list` * [x] `menu` * [x] `menu-surface` * [x] `notched-outline` * [x] `radio` * [x] `ripple` * [x] `select` * [x] `selection-control` * [x] `slider` * [x] `snackbar` * [x] `switch` * [x] `tab` * [x] `tab-bar` * [x] `tab-indicator` * [x] `tab-scroller` * [x] `tab-tabs` _(deprecated)_ * [x] `textfield` * [x] `toolbar` _(deprecated)_ * [x] `top-app-bar` --- .gitignore | 29 +- package.json | 4 +- packages/mdc-animation/index.ts | 104 +-- .../{mdc-drawer => mdc-animation}/types.ts | 30 +- packages/mdc-animation/util.ts | 92 +++ packages/mdc-auto-init/index.ts | 11 +- packages/mdc-base/component.ts | 14 +- .../mdc-base/externs.d.ts | 4 + packages/mdc-base/foundation.ts | 10 +- packages/mdc-base/index.ts | 2 +- packages/mdc-checkbox/adapter.ts | 4 +- packages/mdc-checkbox/component.ts | 185 ++++++ packages/mdc-checkbox/constants.ts | 12 +- packages/mdc-checkbox/foundation.ts | 34 +- packages/mdc-checkbox/index.ts | 160 +---- packages/mdc-chips/chip-set/adapter.ts | 4 +- packages/mdc-chips/chip-set/component.ts | 145 +++++ packages/mdc-chips/chip-set/constants.ts | 6 +- packages/mdc-chips/chip-set/foundation.ts | 4 +- packages/mdc-chips/chip-set/index.ts | 124 +--- packages/mdc-chips/chip/adapter.ts | 4 +- packages/mdc-chips/chip/component.ts | 187 ++++++ packages/mdc-chips/chip/constants.ts | 6 +- packages/mdc-chips/chip/foundation.ts | 4 +- packages/mdc-chips/chip/index.ts | 162 +---- packages/mdc-chips/chip/types.ts | 20 +- packages/mdc-chips/index.ts | 7 +- packages/mdc-dialog/README.md | 8 +- packages/mdc-dialog/adapter.ts | 4 +- packages/mdc-dialog/component.ts | 188 ++++++ packages/mdc-dialog/constants.ts | 8 +- packages/mdc-dialog/foundation.ts | 4 +- packages/mdc-dialog/index.ts | 163 +---- packages/mdc-dialog/types.ts | 14 +- packages/mdc-dialog/util.ts | 8 +- packages/mdc-dom/ponyfill.ts | 8 +- packages/mdc-drawer/adapter.ts | 4 +- packages/mdc-drawer/component.ts | 158 +++++ packages/mdc-drawer/dismissible/foundation.ts | 4 +- packages/mdc-drawer/index.ts | 138 +--- packages/mdc-drawer/modal/foundation.ts | 4 +- packages/mdc-drawer/util.ts | 8 +- .../mdc-feature-targeting/_functions.scss | 3 +- packages/mdc-floating-label/adapter.ts | 6 +- packages/mdc-floating-label/component.ts | 69 ++ packages/mdc-floating-label/constants.ts | 4 +- packages/mdc-floating-label/foundation.ts | 6 +- packages/mdc-floating-label/index.ts | 45 +- packages/mdc-form-field/adapter.ts | 6 +- packages/mdc-form-field/component.ts | 76 +++ packages/mdc-form-field/constants.ts | 6 +- packages/mdc-form-field/foundation.ts | 4 +- packages/mdc-form-field/index.ts | 54 +- packages/mdc-grid-list/adapter.ts | 13 +- packages/mdc-grid-list/component.ts | 54 ++ packages/mdc-grid-list/foundation.ts | 4 +- packages/mdc-grid-list/index.ts | 32 +- packages/mdc-icon-button/adapter.ts | 8 +- packages/mdc-icon-button/component.ts | 84 +++ packages/mdc-icon-button/constants.ts | 6 +- packages/mdc-icon-button/foundation.ts | 4 +- packages/mdc-icon-button/index.ts | 59 +- packages/mdc-icon-button/types.ts | 2 +- packages/mdc-line-ripple/adapter.ts | 6 +- packages/mdc-line-ripple/component.ts | 72 +++ packages/mdc-line-ripple/foundation.ts | 6 +- packages/mdc-line-ripple/index.ts | 48 +- packages/mdc-linear-progress/adapter.ts | 4 +- packages/mdc-linear-progress/component.ts | 70 +++ packages/mdc-linear-progress/foundation.ts | 10 +- packages/mdc-linear-progress/index.ts | 48 +- packages/mdc-list/README.md | 4 +- packages/mdc-list/adapter.ts | 4 +- packages/mdc-list/component.ts | 262 ++++++++ packages/mdc-list/foundation.ts | 16 +- packages/mdc-list/index.ts | 238 +------ packages/mdc-list/types.ts | 9 +- packages/mdc-menu-surface/README.md | 20 +- packages/mdc-menu-surface/adapter.ts | 16 +- packages/mdc-menu-surface/component.ts | 213 +++++++ packages/mdc-menu-surface/foundation.ts | 42 +- packages/mdc-menu-surface/index.ts | 188 +----- packages/mdc-menu-surface/types.ts | 6 +- packages/mdc-menu/README.md | 2 +- packages/mdc-menu/adapter.ts | 8 +- packages/mdc-menu/component.ts | 213 +++++++ packages/mdc-menu/foundation.ts | 4 +- packages/mdc-menu/index.ts | 190 +----- packages/mdc-menu/types.ts | 21 +- packages/mdc-notched-outline/adapter.ts | 4 +- packages/mdc-notched-outline/component.ts | 82 +++ packages/mdc-notched-outline/foundation.ts | 4 +- packages/mdc-notched-outline/index.ts | 58 +- packages/mdc-radio/adapter.ts | 4 +- packages/mdc-radio/component.ts | 112 ++++ packages/mdc-radio/foundation.ts | 8 +- packages/mdc-radio/index.ts | 90 +-- packages/mdc-ripple/adapter.ts | 10 +- packages/mdc-ripple/component.ts | 114 ++++ packages/mdc-ripple/constants.ts | 8 +- packages/mdc-ripple/foundation.ts | 34 +- packages/mdc-ripple/index.ts | 93 +-- packages/mdc-ripple/types.ts | 6 +- packages/mdc-ripple/util.ts | 28 +- packages/mdc-select/adapter.ts | 4 +- packages/mdc-select/component.ts | 595 ++++++++++++++++++ packages/mdc-select/foundation.ts | 8 +- packages/mdc-select/helper-text/adapter.ts | 4 +- packages/mdc-select/helper-text/component.ts | 57 ++ packages/mdc-select/helper-text/foundation.ts | 6 +- packages/mdc-select/helper-text/index.ts | 30 +- packages/mdc-select/icon/adapter.ts | 10 +- packages/mdc-select/icon/component.ts | 58 ++ packages/mdc-select/icon/foundation.ts | 4 +- packages/mdc-select/icon/index.ts | 32 +- packages/mdc-select/index.ts | 582 +---------------- packages/mdc-select/types.ts | 25 +- packages/mdc-selection-control/index.ts | 15 +- .../mdc-selection-control/types.ts | 15 +- packages/mdc-slider/adapter.ts | 6 +- packages/mdc-slider/component.ts | 186 ++++++ packages/mdc-slider/foundation.ts | 8 +- packages/mdc-slider/index.ts | 164 +---- packages/mdc-snackbar/adapter.ts | 4 +- packages/mdc-snackbar/component.ts | 170 +++++ packages/mdc-snackbar/foundation.ts | 4 +- packages/mdc-snackbar/index.ts | 147 +---- packages/mdc-snackbar/types.ts | 14 +- packages/mdc-switch/adapter.ts | 4 +- packages/mdc-switch/component.ts | 129 ++++ packages/mdc-switch/foundation.ts | 4 +- packages/mdc-switch/index.ts | 103 +-- packages/mdc-tab-bar/adapter.ts | 4 +- packages/mdc-tab-bar/component.ts | 177 ++++++ packages/mdc-tab-bar/foundation.ts | 10 +- packages/mdc-tab-bar/index.ts | 152 +---- packages/mdc-tab-bar/types.ts | 12 +- packages/mdc-tab-indicator/adapter.ts | 4 +- packages/mdc-tab-indicator/component.ts | 75 +++ .../mdc-tab-indicator/fading-foundation.ts | 4 +- packages/mdc-tab-indicator/foundation.ts | 4 +- packages/mdc-tab-indicator/index.ts | 53 +- .../mdc-tab-indicator/sliding-foundation.ts | 4 +- packages/mdc-tab-scroller/adapter.ts | 4 +- packages/mdc-tab-scroller/component.ts | 126 ++++ packages/mdc-tab-scroller/foundation.ts | 11 +- packages/mdc-tab-scroller/index.ts | 103 +-- .../mdc-tab-scroller/rtl-default-scroller.ts | 4 +- .../mdc-tab-scroller/rtl-negative-scroller.ts | 4 +- .../mdc-tab-scroller/rtl-reverse-scroller.ts | 4 +- packages/mdc-tab-scroller/rtl-scroller.ts | 4 +- packages/mdc-tab/adapter.ts | 4 +- packages/mdc-tab/component.ts | 145 +++++ packages/mdc-tab/foundation.ts | 4 +- packages/mdc-tab/index.ts | 121 +--- packages/mdc-tab/mdc-tab.scss | 2 - packages/mdc-tab/types.ts | 15 +- packages/mdc-tabs/index.ts | 8 +- packages/mdc-tabs/tab-bar-scroller/adapter.ts | 2 - .../mdc-tabs/tab-bar-scroller/component.ts | 12 +- .../mdc-tabs/tab-bar-scroller/foundation.ts | 12 +- packages/mdc-tabs/tab-bar/adapter.ts | 6 +- packages/mdc-tabs/tab-bar/component.ts | 17 +- packages/mdc-tabs/tab-bar/foundation.ts | 4 +- packages/mdc-tabs/tab-bar/types.ts | 10 +- packages/mdc-tabs/tab/adapter.ts | 2 - packages/mdc-tabs/tab/component.ts | 26 +- packages/mdc-textfield/_mixins.scss | 1 + packages/mdc-textfield/adapter.ts | 65 +- .../character-counter/adapter.ts | 4 +- .../character-counter/component.ts | 50 ++ .../character-counter/foundation.ts | 4 +- .../mdc-textfield/character-counter/index.ts | 21 +- packages/mdc-textfield/component.ts | 461 ++++++++++++++ packages/mdc-textfield/foundation.ts | 18 +- packages/mdc-textfield/helper-text/adapter.ts | 4 +- .../mdc-textfield/helper-text/component.ts | 57 ++ .../mdc-textfield/helper-text/foundation.ts | 4 +- packages/mdc-textfield/helper-text/index.ts | 30 +- packages/mdc-textfield/icon/adapter.ts | 8 +- packages/mdc-textfield/icon/component.ts | 58 ++ packages/mdc-textfield/icon/foundation.ts | 4 +- packages/mdc-textfield/icon/index.ts | 37 +- packages/mdc-textfield/index.ts | 421 +------------ packages/mdc-textfield/mdc-text-field.scss | 1 + packages/mdc-textfield/types.ts | 20 +- packages/mdc-toolbar/README.md | 4 +- packages/mdc-toolbar/adapter.ts | 6 +- packages/mdc-toolbar/component.ts | 8 +- packages/mdc-toolbar/foundation.ts | 20 +- packages/mdc-toolbar/types.ts | 8 +- packages/mdc-top-app-bar/adapter.ts | 4 +- packages/mdc-top-app-bar/component.ts | 120 ++++ packages/mdc-top-app-bar/fixed/foundation.ts | 4 +- packages/mdc-top-app-bar/foundation.ts | 8 +- packages/mdc-top-app-bar/index.ts | 106 +--- packages/mdc-top-app-bar/short/foundation.ts | 4 +- .../mdc-top-app-bar/standard/foundation.ts | 4 +- ...e-declaration-statements-for-typescript.js | 223 ------- ...rite-sass-import-statements-for-closure.js | 75 --- scripts/sass-closure-rewriter.sh | 51 -- scripts/travis-env-vars.sh | 2 +- scripts/typescript-rewrite.sh | 46 -- test/screenshot/.gitignore | 1 + test/screenshot/infra/commands/test.js | 2 - test/screenshot/infra/lib/diff-base-parser.js | 2 +- test/screenshot/infra/lib/github-api.js | 54 +- test/screenshot/infra/lib/local-storage.js | 2 +- test/screenshot/infra/lib/report-builder.js | 13 - .../spec/mdc-textfield/fixture.scss | 2 +- .../mdc-chips/mdc-chip-set.foundation.test.js | 2 +- test/unit/mdc-dialog/foundation.test.js | 2 +- test/unit/mdc-drawer/modal.foundation.test.js | 2 +- .../mdc-floating-label-foundation.test.js | 2 +- test/unit/mdc-grid-list/foundation.test.js | 2 +- .../mdc-line-ripple-foundation.test.js | 2 +- .../menu-surface.foundation.test.js | 2 +- .../mdc-notched-outline/foundation.test.js | 2 +- test/unit/mdc-select/foundation.test.js | 2 +- .../mdc-select/mdc-select-enhanced.test.js | 2 +- .../mdc-select-helper-text-foundation.test.js | 2 +- .../mdc-select-icon-foundation.test.js | 2 +- test/unit/mdc-slider/foundation.test.js | 2 +- test/unit/mdc-slider/helpers.js | 2 +- test/unit/mdc-snackbar/foundation.test.js | 2 +- test/unit/mdc-tab-bar/foundation.test.js | 2 +- test/unit/mdc-tab-scroller/foundation.test.js | 8 +- .../mdc-tab-scroller/mdc-tab-scroller.test.js | 2 +- .../rtl-default-scroller.test.js | 4 +- .../rtl-negative-scroller.test.js | 4 +- .../rtl-reverse-scroller.test.js | 4 +- test/unit/mdc-tab/foundation.test.js | 2 +- .../mdc-tabs/mdc-tab-bar-foundation.test.js | 2 +- .../mdc-tab-bar-scroller-foundation.test.js | 2 +- test/unit/mdc-tabs/mdc-tab-foundation.test.js | 2 +- test/unit/mdc-textfield/foundation.test.js | 2 +- ...field-character-counter-foundation.test.js | 2 +- ...-text-field-helper-text-foundation.test.js | 2 +- .../mdc-text-field-icon-foundation.test.js | 2 +- test/unit/mdc-toolbar/foundation.test.js | 2 +- .../mdc-top-app-bar/fixed.foundation.test.js | 4 +- test/unit/mdc-top-app-bar/foundation.test.js | 2 +- .../mdc-top-app-bar/mdc-top-app-bar.test.js | 6 +- .../mdc-top-app-bar/short.foundation.test.js | 4 +- .../standard.foundation.test.js | 2 +- 245 files changed, 5600 insertions(+), 5073 deletions(-) rename packages/{mdc-drawer => mdc-animation}/types.ts (56%) create mode 100644 packages/mdc-animation/util.ts rename typings/dom.ie.d.ts => packages/mdc-base/externs.d.ts (96%) create mode 100644 packages/mdc-checkbox/component.ts create mode 100644 packages/mdc-chips/chip-set/component.ts create mode 100644 packages/mdc-chips/chip/component.ts create mode 100644 packages/mdc-dialog/component.ts create mode 100644 packages/mdc-drawer/component.ts create mode 100644 packages/mdc-floating-label/component.ts create mode 100644 packages/mdc-form-field/component.ts create mode 100644 packages/mdc-grid-list/component.ts create mode 100644 packages/mdc-icon-button/component.ts create mode 100644 packages/mdc-line-ripple/component.ts create mode 100644 packages/mdc-linear-progress/component.ts create mode 100644 packages/mdc-list/component.ts create mode 100644 packages/mdc-menu-surface/component.ts create mode 100644 packages/mdc-menu/component.ts create mode 100644 packages/mdc-notched-outline/component.ts create mode 100644 packages/mdc-radio/component.ts create mode 100644 packages/mdc-ripple/component.ts create mode 100644 packages/mdc-select/component.ts create mode 100644 packages/mdc-select/helper-text/component.ts create mode 100644 packages/mdc-select/icon/component.ts rename typings/custom.d.ts => packages/mdc-selection-control/types.ts (78%) create mode 100644 packages/mdc-slider/component.ts create mode 100644 packages/mdc-snackbar/component.ts create mode 100644 packages/mdc-switch/component.ts create mode 100644 packages/mdc-tab-bar/component.ts create mode 100644 packages/mdc-tab-indicator/component.ts create mode 100644 packages/mdc-tab-scroller/component.ts create mode 100644 packages/mdc-tab/component.ts create mode 100644 packages/mdc-textfield/character-counter/component.ts create mode 100644 packages/mdc-textfield/component.ts create mode 100644 packages/mdc-textfield/helper-text/component.ts create mode 100644 packages/mdc-textfield/icon/component.ts create mode 100644 packages/mdc-top-app-bar/component.ts delete mode 100644 scripts/rewrite-declaration-statements-for-typescript.js delete mode 100644 scripts/rewrite-sass-import-statements-for-closure.js delete mode 100755 scripts/sass-closure-rewriter.sh delete mode 100755 scripts/typescript-rewrite.sh diff --git a/.gitignore b/.gitignore index f0a6e5eb0f1..c4c631a5c66 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,22 @@ -node_modules +# macOS filesystem .DS_Store -/build -/coverage -packages/*/dist -out + +# Editors and IDEs +.idea/ +.vscode/ + +# Generated files *.log -.idea *.sw* -.closure-tmp -.site-generator-tmp -.typescript-tmp -.vscode +node_modules/ + +# Build output +/build/ +/coverage/ +packages/*/dist + +# Material.io site generator test (`npm run test:site`) +.site-generator-tmp/ + +# Used by internal sync & rewrite scripts +.rewrite-tmp/ diff --git a/package.json b/package.json index 4244f6a85ec..433977c797a 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "build:demos": "npm run clean && mkdirp build && webpack --config=demos/webpack.config.js --progress --colors", "build:min": "mkdirp build && cross-env MDC_ENV=production webpack -p --progress --colors", "changelog": "standard-changelog -i CHANGELOG.md -k packages/material-components-web/package.json", - "clean": "del-cli build/** build .typescript-tmp/** .typescript-tmp", + "clean": "del-cli build/** build .rewrite-tmp/** .rewrite-tmp", "clean:site": "del-cli .site-generator-tmp/** .site-generator-tmp", "dist": "npm run build && npm run build:min", "dev": "npm run clean && cross-env MDC_ENV=development webpack-dev-server --config=demos/webpack.config.js --progress --inline --hot --host 0.0.0.0", @@ -16,7 +16,7 @@ "fix:css": "stylelint --fix \"packages/**/*.scss\"; stylelint --fix --config=test/screenshot/.stylelintrc.yaml \"test/screenshot/**/*.scss\"", "fix": "npm-run-all --parallel fix:*", "lint:css": "stylelint \"packages/**/*.scss\" && stylelint --config=test/screenshot/.stylelintrc.yaml \"test/screenshot/**/*.scss\"", - "lint:js": "eslint packages test scripts webpack.config.js demos/webpack.config.js karma.conf.js", + "lint:js": "eslint test scripts webpack.config.js demos/webpack.config.js karma.conf.js", "lint:ts": "tslint --exclude \"test/**/*.d.ts\" \"packages/**/*.ts\" \"test/**/*.ts\" \"scripts/**/*.ts\"", "lint:html": "find test/screenshot/spec/ -name '*.html' | grep -v 'index.html$' | xargs htmllint", "lint:imports": "node scripts/check-imports.js", diff --git a/packages/mdc-animation/index.ts b/packages/mdc-animation/index.ts index 0af92fa2b2a..a95d78c3c93 100644 --- a/packages/mdc-animation/index.ts +++ b/packages/mdc-animation/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2016 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,102 +21,8 @@ * THE SOFTWARE. */ -type StandardCssPropertyName = ( - 'animation' | 'transform' | 'transition' -); -type PrefixedCssPropertyName = ( - '-webkit-animation' | '-webkit-transform' | '-webkit-transition' -); -type StandardJsEventType = ( - 'animationend' | 'animationiteration' | 'animationstart' | 'transitionend' -); -type PrefixedJsEventType = ( - 'webkitAnimationEnd' | 'webkitAnimationIteration' | 'webkitAnimationStart' | 'webkitTransitionEnd' -); +import * as util from './util'; -type CssVendorPropertyMap = { [K in StandardCssPropertyName]: CssVendorProperty }; -type JsVendorPropertyMap = { [K in StandardJsEventType]: JsVendorProperty }; - -interface CssVendorProperty { - prefixed: PrefixedCssPropertyName; - standard: StandardCssPropertyName; -} - -interface JsVendorProperty { - cssProperty: StandardCssPropertyName; - prefixed: PrefixedJsEventType; - standard: StandardJsEventType; -} - -const cssPropertyNameMap: CssVendorPropertyMap = { - animation: { - prefixed: '-webkit-animation', - standard: 'animation', - }, - transform: { - prefixed: '-webkit-transform', - standard: 'transform', - }, - transition: { - prefixed: '-webkit-transition', - standard: 'transition', - }, -}; - -const jsEventTypeMap: JsVendorPropertyMap = { - animationend: { - cssProperty: 'animation', - prefixed: 'webkitAnimationEnd', - standard: 'animationend', - }, - animationiteration: { - cssProperty: 'animation', - prefixed: 'webkitAnimationIteration', - standard: 'animationiteration', - }, - animationstart: { - cssProperty: 'animation', - prefixed: 'webkitAnimationStart', - standard: 'animationstart', - }, - transitionend: { - cssProperty: 'transition', - prefixed: 'webkitTransitionEnd', - standard: 'transitionend', - }, -}; - -function isWindow(windowObj: Window): boolean { - return Boolean(windowObj.document) && typeof windowObj.document.createElement === 'function'; -} - -function getCorrectPropertyName(windowObj: Window, cssProperty: StandardCssPropertyName): - StandardCssPropertyName | PrefixedCssPropertyName { - if (isWindow(windowObj) && cssProperty in cssPropertyNameMap) { - const el = windowObj.document.createElement('div'); - const {standard, prefixed} = cssPropertyNameMap[cssProperty]; - const isStandard = standard in el.style; - return isStandard ? standard : prefixed; - } - return cssProperty; -} - -function getCorrectEventName(windowObj: Window, eventType: StandardJsEventType): - StandardJsEventType | PrefixedJsEventType { - if (isWindow(windowObj) && eventType in jsEventTypeMap) { - const el = windowObj.document.createElement('div'); - const {standard, prefixed, cssProperty} = jsEventTypeMap[eventType]; - const isStandard = cssProperty in el.style; - return isStandard ? standard : prefixed; - } - return eventType; -} - -export { - PrefixedCssPropertyName, - StandardCssPropertyName, - PrefixedJsEventType, - StandardJsEventType, - getCorrectEventName, - getCorrectPropertyName, -}; +export {util}; // New namespace +export * from './types'; +export * from './util'; // Old namespace for backward compatibility diff --git a/packages/mdc-drawer/types.ts b/packages/mdc-animation/types.ts similarity index 56% rename from packages/mdc-drawer/types.ts rename to packages/mdc-animation/types.ts index ea4926e68b3..20dc6a96ea8 100644 --- a/packages/mdc-drawer/types.ts +++ b/packages/mdc-animation/types.ts @@ -21,12 +21,28 @@ * THE SOFTWARE. */ -import {MDCList} from '@material/list/index'; -import * as FocusTrapLib from 'focus-trap'; +export type StandardCssPropertyName = + 'animation' | 'transform' | 'transition'; -export type FocusTrapFactory = ( - element: HTMLElement | string, - userOptions?: FocusTrapLib.Options, -) => FocusTrapLib.FocusTrap; +export type PrefixedCssPropertyName = + '-webkit-animation' | '-webkit-transform' | '-webkit-transition'; -export type ListFactory = (el: Element) => MDCList; +export type StandardJsEventType = + 'animationend' | 'animationiteration' | 'animationstart' | 'transitionend'; + +export type PrefixedJsEventType = + 'webkitAnimationEnd' | 'webkitAnimationIteration' | 'webkitAnimationStart' | 'webkitTransitionEnd'; + +export interface CssVendorProperty { + prefixed: PrefixedCssPropertyName; + standard: StandardCssPropertyName; +} + +export interface JsVendorProperty { + cssProperty: StandardCssPropertyName; + prefixed: PrefixedJsEventType; + standard: StandardJsEventType; +} + +export type CssVendorPropertyMap = { [K in StandardCssPropertyName]: CssVendorProperty }; +export type JsVendorPropertyMap = { [K in StandardJsEventType]: JsVendorProperty }; diff --git a/packages/mdc-animation/util.ts b/packages/mdc-animation/util.ts new file mode 100644 index 00000000000..553b1e68170 --- /dev/null +++ b/packages/mdc-animation/util.ts @@ -0,0 +1,92 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import { + CssVendorPropertyMap, JsVendorPropertyMap, + PrefixedCssPropertyName, PrefixedJsEventType, + StandardCssPropertyName, StandardJsEventType, +} from './types'; + +const cssPropertyNameMap: CssVendorPropertyMap = { + animation: { + prefixed: '-webkit-animation', + standard: 'animation', + }, + transform: { + prefixed: '-webkit-transform', + standard: 'transform', + }, + transition: { + prefixed: '-webkit-transition', + standard: 'transition', + }, +}; + +const jsEventTypeMap: JsVendorPropertyMap = { + animationend: { + cssProperty: 'animation', + prefixed: 'webkitAnimationEnd', + standard: 'animationend', + }, + animationiteration: { + cssProperty: 'animation', + prefixed: 'webkitAnimationIteration', + standard: 'animationiteration', + }, + animationstart: { + cssProperty: 'animation', + prefixed: 'webkitAnimationStart', + standard: 'animationstart', + }, + transitionend: { + cssProperty: 'transition', + prefixed: 'webkitTransitionEnd', + standard: 'transitionend', + }, +}; + +function isWindow(windowObj: Window): boolean { + return Boolean(windowObj.document) && typeof windowObj.document.createElement === 'function'; +} + +export function getCorrectPropertyName(windowObj: Window, cssProperty: StandardCssPropertyName): + StandardCssPropertyName | PrefixedCssPropertyName { + if (isWindow(windowObj) && cssProperty in cssPropertyNameMap) { + const el = windowObj.document.createElement('div'); + const {standard, prefixed} = cssPropertyNameMap[cssProperty]; + const isStandard = standard in el.style; + return isStandard ? standard : prefixed; + } + return cssProperty; +} + +export function getCorrectEventName(windowObj: Window, eventType: StandardJsEventType): + StandardJsEventType | PrefixedJsEventType { + if (isWindow(windowObj) && eventType in jsEventTypeMap) { + const el = windowObj.document.createElement('div'); + const {standard, prefixed, cssProperty} = jsEventTypeMap[eventType]; + const isStandard = cssProperty in el.style; + return isStandard ? standard : prefixed; + } + return eventType; +} diff --git a/packages/mdc-auto-init/index.ts b/packages/mdc-auto-init/index.ts index 18bcba6a74b..a17d63251a3 100644 --- a/packages/mdc-auto-init/index.ts +++ b/packages/mdc-auto-init/index.ts @@ -23,11 +23,13 @@ // tslint:disable:only-arrow-functions -import {MDCComponent, MDCFoundation} from '@material/base/index'; +import {MDCComponent} from '@material/base/component'; +import {MDCFoundation} from '@material/base/foundation'; interface ComponentClass { // tslint:disable-next-line:no-any a component can pass in anything it needs to the constructor new(root: Element, foundation?: F, ...args: any[]): MDCComponent; + attachTo(root: Element): MDCComponent; } @@ -58,6 +60,7 @@ function _emit(evtType: string, evtData: T, shouldBubble = fal /** * Auto-initializes all MDC components on a page. */ + export function mdcAutoInit(root = document, warn = CONSOLE_WARN) { const components = []; const nodes: Element[] = [].slice.call(root.querySelectorAll('[data-mdc-auto-init]')); @@ -71,7 +74,7 @@ export function mdcAutoInit(root = document, warn = CONSOLE_WARN) { const Constructor = registry[ctorName]; // tslint:disable-line:variable-name if (typeof Constructor !== 'function') { throw new Error( - `(mdc-auto-init) Could not find constructor in registry for ${ctorName}`); + `(mdc-auto-init) Could not find constructor in registry for ${ctorName}`); } if (Object.getOwnPropertyDescriptor(node, ctorName)) { @@ -103,8 +106,8 @@ mdcAutoInit.register = function(componentName: string, Constructor: ComponentCla } if (registry[componentName]) { warn( - `(mdc-auto-init) Overriding registration for ${componentName} with ${Constructor}. ` + - `Was: ${registry[componentName]}`); + `(mdc-auto-init) Overriding registration for ${componentName} with ${Constructor}. ` + + `Was: ${registry[componentName]}`); } registry[componentName] = Constructor; }; diff --git a/packages/mdc-base/component.ts b/packages/mdc-base/component.ts index 80b0402fb1a..1b25b3f5b19 100644 --- a/packages/mdc-base/component.ts +++ b/packages/mdc-base/component.ts @@ -24,7 +24,7 @@ import {MDCFoundation} from './foundation'; import {CustomEventListener, EventType, SpecificEventListener} from './types'; -class MDCComponent { +export class MDCComponent { static attachTo(root: Element): MDCComponent> { // Subclasses which extend MDCBase should provide an attachTo() method that takes a root element and // returns an instantiated component with its root set to that element. Also note that in the cases of @@ -37,10 +37,10 @@ class MDCComponent { protected foundation_: FoundationType; constructor( - root: Element, - foundation?: FoundationType, - // tslint:disable-next-line:no-any a component can pass in anything it needs to the constructor - ...args: any[] + root: Element, + foundation?: FoundationType, + // tslint:disable-next-line:no-any a component can pass in anything it needs to the constructor + ...args: any[] ) { this.root_ = root; this.initialize(...args); @@ -63,7 +63,7 @@ class MDCComponent { // Subclasses must override this method to return a properly configured foundation class for the // component. throw new Error('Subclasses must override getDefaultFoundation to return a properly configured ' + - 'foundation class'); + 'foundation class'); } initialSyncWithDOM() { @@ -118,4 +118,4 @@ class MDCComponent { } } -export {MDCComponent as default, MDCComponent}; +export default MDCComponent; diff --git a/typings/dom.ie.d.ts b/packages/mdc-base/externs.d.ts similarity index 96% rename from typings/dom.ie.d.ts rename to packages/mdc-base/externs.d.ts index 0ff50d00190..6383f026165 100644 --- a/typings/dom.ie.d.ts +++ b/packages/mdc-base/externs.d.ts @@ -24,3 +24,7 @@ declare interface Element { msMatchesSelector?: (selector: string) => boolean; } + +declare interface Window { + CSS: CSS; +} diff --git a/packages/mdc-base/foundation.ts b/packages/mdc-base/foundation.ts index 89decf96400..2fbca84e28e 100644 --- a/packages/mdc-base/foundation.ts +++ b/packages/mdc-base/foundation.ts @@ -21,20 +21,20 @@ * THE SOFTWARE. */ -class MDCFoundation { - static get cssClasses(): {[key: string]: string} { +export class MDCFoundation { + static get cssClasses(): { [key: string]: string } { // Classes extending MDCFoundation should implement this method to return an object which exports every // CSS class the foundation class needs as a property. e.g. {ACTIVE: 'mdc-component--active'} return {}; } - static get strings(): {[key: string]: string} { + static get strings(): { [key: string]: string } { // Classes extending MDCFoundation should implement this method to return an object which exports all // semantic strings as constants. e.g. {ARIA_ROLE: 'tablist'} return {}; } - static get numbers(): {[key: string]: number} { + static get numbers(): { [key: string]: number } { // Classes extending MDCFoundation should implement this method to return an object which exports all // of its semantic numbers as constants. e.g. {ANIMATION_DELAY_MS: 350} return {}; @@ -62,4 +62,4 @@ class MDCFoundation { } } -export {MDCFoundation as default, MDCFoundation}; +export default MDCFoundation; diff --git a/packages/mdc-base/index.ts b/packages/mdc-base/index.ts index 9b48a913e35..71bd20cf62c 100644 --- a/packages/mdc-base/index.ts +++ b/packages/mdc-base/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2016 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal diff --git a/packages/mdc-checkbox/adapter.ts b/packages/mdc-checkbox/adapter.ts index b9feff6fa9f..e68f7f3183b 100644 --- a/packages/mdc-checkbox/adapter.ts +++ b/packages/mdc-checkbox/adapter.ts @@ -28,7 +28,7 @@ * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCCheckboxAdapter { +export interface MDCCheckboxAdapter { addClass(className: string): void; forceLayout(): void; hasNativeControl(): boolean; @@ -40,5 +40,3 @@ interface MDCCheckboxAdapter { setNativeControlAttr(attr: string, value: string): void; setNativeControlDisabled(disabled: boolean): void; } - -export {MDCCheckboxAdapter as default, MDCCheckboxAdapter}; diff --git a/packages/mdc-checkbox/component.ts b/packages/mdc-checkbox/component.ts new file mode 100644 index 00000000000..f2a0d4614d0 --- /dev/null +++ b/packages/mdc-checkbox/component.ts @@ -0,0 +1,185 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {getCorrectEventName} from '@material/animation/util'; +import {MDCComponent} from '@material/base/component'; +import {ponyfill} from '@material/dom/index'; +import {MDCRipple, MDCRippleAdapter, MDCRippleCapableSurface, MDCRippleFoundation} from '@material/ripple/index'; +import {MDCSelectionControl} from '@material/selection-control/types'; +import {MDCCheckboxAdapter} from './adapter'; +import {MDCCheckboxFoundation} from './foundation'; + +const {NATIVE_CONTROL_SELECTOR} = MDCCheckboxFoundation.strings; + +const CB_PROTO_PROPS = ['checked', 'indeterminate']; + +export class MDCCheckbox extends MDCComponent + implements MDCSelectionControl, MDCRippleCapableSurface { + + static attachTo(root: Element) { + return new MDCCheckbox(root); + } + + /** + * Returns the state of the native control element, or null if the native control element is not present. + */ + get nativeCb_(): HTMLInputElement { + const cbEl = this.root_.querySelector(NATIVE_CONTROL_SELECTOR); + if (!cbEl) { + throw new Error(`Checkbox requires a ${NATIVE_CONTROL_SELECTOR} element`); + } + return cbEl; + } + + // Public visibility for this property is required by MDCRippleCapableSurface. + root_!: Element; // assigned in MDCComponent constructor + + private readonly ripple_: MDCRipple = this.createRipple_(); + private handleChange_!: EventListener; // assigned in initialSyncWithDOM() + private handleAnimationEnd_!: EventListener; // assigned in initialSyncWithDOM() + + initialSyncWithDOM() { + this.handleChange_ = () => this.foundation_.handleChange(); + this.handleAnimationEnd_ = () => this.foundation_.handleAnimationEnd(); + this.nativeCb_.addEventListener('change', this.handleChange_); + this.listen(getCorrectEventName(window, 'animationend'), this.handleAnimationEnd_); + this.installPropertyChangeHooks_(); + } + + destroy() { + this.ripple_.destroy(); + this.nativeCb_.removeEventListener('change', this.handleChange_); + this.unlisten(getCorrectEventName(window, 'animationend'), this.handleAnimationEnd_); + this.uninstallPropertyChangeHooks_(); + super.destroy(); + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + const adapter: MDCCheckboxAdapter = { + addClass: (className) => this.root_.classList.add(className), + forceLayout: () => (this.root_ as HTMLElement).offsetWidth, + hasNativeControl: () => !!this.nativeCb_, + isAttachedToDOM: () => Boolean(this.root_.parentNode), + isChecked: () => this.checked, + isIndeterminate: () => this.indeterminate, + removeClass: (className) => this.root_.classList.remove(className), + removeNativeControlAttr: (attr) => this.nativeCb_.removeAttribute(attr), + setNativeControlAttr: (attr, value) => this.nativeCb_.setAttribute(attr, value), + setNativeControlDisabled: (disabled) => this.nativeCb_.disabled = disabled, + }; + return new MDCCheckboxFoundation(adapter); + } + + private createRipple_(): MDCRipple { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + const adapter: MDCRippleAdapter = { + ...MDCRipple.createAdapter(this), + deregisterInteractionHandler: (evtType, handler) => this.nativeCb_.removeEventListener(evtType, handler), + isSurfaceActive: () => ponyfill.matches(this.nativeCb_, ':active'), + isUnbounded: () => true, + registerInteractionHandler: (evtType, handler) => this.nativeCb_.addEventListener(evtType, handler), + }; + return new MDCRipple(this.root_, new MDCRippleFoundation(adapter)); + } + + private installPropertyChangeHooks_() { + const nativeCb = this.nativeCb_; + const cbProto = Object.getPrototypeOf(nativeCb); + + CB_PROTO_PROPS.forEach((controlState) => { + const desc = Object.getOwnPropertyDescriptor(cbProto, controlState); + // We have to check for this descriptor, since some browsers (Safari) don't support its return. + // See: https://bugs.webkit.org/show_bug.cgi?id=49739 + if (!validDescriptor(desc)) { + return; + } + + const nativeCbDesc = { + configurable: desc.configurable, + enumerable: desc.enumerable, + get: desc.get, + set: (state: boolean) => { + desc.set!.call(nativeCb, state); + this.foundation_.handleChange(); + }, + }; + Object.defineProperty(nativeCb, controlState, nativeCbDesc); + }); + } + + private uninstallPropertyChangeHooks_() { + const nativeCb = this.nativeCb_; + const cbProto = Object.getPrototypeOf(nativeCb); + + CB_PROTO_PROPS.forEach((controlState) => { + const desc = Object.getOwnPropertyDescriptor(cbProto, controlState); + if (!validDescriptor(desc)) { + return; + } + Object.defineProperty(nativeCb, controlState, desc); + }); + } + + get ripple(): MDCRipple { + return this.ripple_; + } + + get checked(): boolean { + return this.nativeCb_.checked; + } + + set checked(checked: boolean) { + this.nativeCb_.checked = checked; + } + + get indeterminate(): boolean { + return this.nativeCb_.indeterminate; + } + + set indeterminate(indeterminate: boolean) { + this.nativeCb_.indeterminate = indeterminate; + } + + get disabled(): boolean { + return this.nativeCb_.disabled; + } + + set disabled(disabled: boolean) { + this.foundation_.setDisabled(disabled); + } + + get value(): string { + return this.nativeCb_.value; + } + + set value(value: string) { + this.nativeCb_.value = value; + } +} + +function validDescriptor(inputPropDesc: PropertyDescriptor | undefined): inputPropDesc is PropertyDescriptor { + return !!inputPropDesc && typeof inputPropDesc.set === 'function'; +} diff --git a/packages/mdc-checkbox/constants.ts b/packages/mdc-checkbox/constants.ts index f35555dbd12..15ec2b69c85 100644 --- a/packages/mdc-checkbox/constants.ts +++ b/packages/mdc-checkbox/constants.ts @@ -21,9 +21,7 @@ * THE SOFTWARE. */ -const ROOT = 'mdc-checkbox'; - -const cssClasses = { +export const cssClasses = { ANIM_CHECKED_INDETERMINATE: 'mdc-checkbox--anim-checked-indeterminate', ANIM_CHECKED_UNCHECKED: 'mdc-checkbox--anim-checked-unchecked', ANIM_INDETERMINATE_CHECKED: 'mdc-checkbox--anim-indeterminate-checked', @@ -36,18 +34,16 @@ const cssClasses = { UPGRADED: 'mdc-checkbox--upgraded', }; -const strings = { +export const strings = { ARIA_CHECKED_ATTR: 'aria-checked', ARIA_CHECKED_INDETERMINATE_VALUE: 'mixed', - NATIVE_CONTROL_SELECTOR: `.${ROOT}__native-control`, + NATIVE_CONTROL_SELECTOR: '.mdc-checkbox__native-control', TRANSITION_STATE_CHECKED: 'checked', TRANSITION_STATE_INDETERMINATE: 'indeterminate', TRANSITION_STATE_INIT: 'init', TRANSITION_STATE_UNCHECKED: 'unchecked', }; -const numbers = { +export const numbers = { ANIM_END_LATCH_MS: 250, }; - -export {cssClasses, strings, numbers}; diff --git a/packages/mdc-checkbox/foundation.ts b/packages/mdc-checkbox/foundation.ts index f99d745e833..429110c8f86 100644 --- a/packages/mdc-checkbox/foundation.ts +++ b/packages/mdc-checkbox/foundation.ts @@ -25,7 +25,7 @@ import {MDCFoundation} from '@material/base/foundation'; import {MDCCheckboxAdapter} from './adapter'; import {cssClasses, numbers, strings} from './constants'; -class MDCCheckboxFoundation extends MDCFoundation { +export class MDCCheckboxFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } @@ -164,20 +164,20 @@ class MDCCheckboxFoundation extends MDCFoundation { } = MDCCheckboxFoundation.cssClasses; switch (oldState) { - // @ts-ignore:no-switch-case-fall-through already existing fallthrough - case TRANSITION_STATE_INIT: - if (newState === TRANSITION_STATE_UNCHECKED) { - return ''; - } - // fallthrough - case TRANSITION_STATE_UNCHECKED: - return newState === TRANSITION_STATE_CHECKED ? ANIM_UNCHECKED_CHECKED : ANIM_UNCHECKED_INDETERMINATE; - case TRANSITION_STATE_CHECKED: - return newState === TRANSITION_STATE_UNCHECKED ? ANIM_CHECKED_UNCHECKED : ANIM_CHECKED_INDETERMINATE; - // TRANSITION_STATE_INDETERMINATE - default: - return newState === TRANSITION_STATE_CHECKED ? - ANIM_INDETERMINATE_CHECKED : ANIM_INDETERMINATE_UNCHECKED; + // @ts-ignore:no-switch-case-fall-through already existing fallthrough + case TRANSITION_STATE_INIT: + if (newState === TRANSITION_STATE_UNCHECKED) { + return ''; + } + // fallthrough + case TRANSITION_STATE_UNCHECKED: + return newState === TRANSITION_STATE_CHECKED ? ANIM_UNCHECKED_CHECKED : ANIM_UNCHECKED_INDETERMINATE; + case TRANSITION_STATE_CHECKED: + return newState === TRANSITION_STATE_UNCHECKED ? ANIM_CHECKED_UNCHECKED : ANIM_CHECKED_INDETERMINATE; + // TRANSITION_STATE_INDETERMINATE + default: + return newState === TRANSITION_STATE_CHECKED ? + ANIM_INDETERMINATE_CHECKED : ANIM_INDETERMINATE_UNCHECKED; } } @@ -185,7 +185,7 @@ class MDCCheckboxFoundation extends MDCFoundation { // Ensure aria-checked is set to mixed if checkbox is in indeterminate state. if (this.adapter_.isIndeterminate()) { this.adapter_.setNativeControlAttr( - strings.ARIA_CHECKED_ATTR, strings.ARIA_CHECKED_INDETERMINATE_VALUE); + strings.ARIA_CHECKED_ATTR, strings.ARIA_CHECKED_INDETERMINATE_VALUE); } else { // The on/off state does not need to keep track of aria-checked, since // the screenreader uses the checked property on the checkbox element. @@ -194,4 +194,4 @@ class MDCCheckboxFoundation extends MDCFoundation { } } -export {MDCCheckboxFoundation as default, MDCCheckboxFoundation}; +export default MDCCheckboxFoundation; diff --git a/packages/mdc-checkbox/index.ts b/packages/mdc-checkbox/index.ts index 79bf2b33ea9..f8c89ac94f3 100644 --- a/packages/mdc-checkbox/index.ts +++ b/packages/mdc-checkbox/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2016 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,162 +21,6 @@ * THE SOFTWARE. */ -import {getCorrectEventName} from '@material/animation/index'; -import {MDCComponent} from '@material/base/component'; -import {EventType, SpecificEventListener} from '@material/base/index'; -import {ponyfill} from '@material/dom/index'; -import {MDCRipple, MDCRippleFoundation, RippleCapableSurface} from '@material/ripple/index'; -import {MDCSelectionControl} from '@material/selection-control/index'; -import {MDCCheckboxFoundation} from './foundation'; -const CB_PROTO_PROPS = ['checked', 'indeterminate']; - -class MDCCheckbox extends MDCComponent implements MDCSelectionControl, RippleCapableSurface { - static attachTo(root: Element) { - return new MDCCheckbox(root); - } - - /** - * Returns the state of the native control element, or null if the native control element is not present. - */ - get nativeCb_(): HTMLInputElement { - const {NATIVE_CONTROL_SELECTOR} = MDCCheckboxFoundation.strings; - const cbEl = this.root_.querySelector(NATIVE_CONTROL_SELECTOR); - if (!cbEl) { - throw new Error(`Checkbox requires a ${NATIVE_CONTROL_SELECTOR} element`); - } - return cbEl; - } - - // Public visibility for this property is required by RippleCapableSurface. - root_!: Element; // assigned in MDCComponent constructor - - private ripple_: MDCRipple = this.initRipple_(); - private handleChange_!: EventListener; // assigned in initialSyncWithDOM() - private handleAnimationEnd_!: EventListener; // assigned in initialSyncWithDOM() - - initialSyncWithDOM() { - this.handleChange_ = () => this.foundation_.handleChange(); - this.handleAnimationEnd_ = () => this.foundation_.handleAnimationEnd(); - this.nativeCb_.addEventListener('change', this.handleChange_); - this.listen(getCorrectEventName(window, 'animationend'), this.handleAnimationEnd_); - this.installPropertyChangeHooks_(); - } - - destroy() { - this.ripple_.destroy(); - this.nativeCb_.removeEventListener('change', this.handleChange_); - this.unlisten(getCorrectEventName(window, 'animationend'), this.handleAnimationEnd_); - this.uninstallPropertyChangeHooks_(); - super.destroy(); - } - - getDefaultFoundation(): MDCCheckboxFoundation { - return new MDCCheckboxFoundation({ - addClass: (className) => this.root_.classList.add(className), - forceLayout: () => (this.root_ as HTMLElement).offsetWidth, - hasNativeControl: () => !!this.nativeCb_, - isAttachedToDOM: () => Boolean(this.root_.parentNode), - isChecked: () => this.checked, - isIndeterminate: () => this.indeterminate, - removeClass: (className) => this.root_.classList.remove(className), - removeNativeControlAttr: (attr) => this.nativeCb_.removeAttribute(attr), - setNativeControlAttr: (attr, value) => this.nativeCb_.setAttribute(attr, value), - setNativeControlDisabled: (disabled) => this.nativeCb_.disabled = disabled, - }); - } - - private initRipple_(): MDCRipple { - const foundation = new MDCRippleFoundation({ - ...MDCRipple.createAdapter(this), - deregisterInteractionHandler: (evtType: K, handler: SpecificEventListener) => - this.nativeCb_.removeEventListener(evtType, handler), - isSurfaceActive: () => ponyfill.matches(this.nativeCb_, ':active'), - isUnbounded: () => true, - registerInteractionHandler: (evtType: K, handler: SpecificEventListener) => - this.nativeCb_.addEventListener(evtType, handler), - }); - return new MDCRipple(this.root_, foundation); - } - - private installPropertyChangeHooks_() { - const nativeCb = this.nativeCb_; - const cbProto = Object.getPrototypeOf(nativeCb); - - CB_PROTO_PROPS.forEach((controlState) => { - const desc = Object.getOwnPropertyDescriptor(cbProto, controlState); - // We have to check for this descriptor, since some browsers (Safari) don't support its return. - // See: https://bugs.webkit.org/show_bug.cgi?id=49739 - if (!validDescriptor(desc)) { - return; - } - - const nativeCbDesc = { - configurable: desc.configurable, - enumerable: desc.enumerable, - get: desc.get, - set: (state: boolean) => { - desc.set!.call(nativeCb, state); - this.foundation_.handleChange(); - }, - }; - Object.defineProperty(nativeCb, controlState, nativeCbDesc); - }); - } - - private uninstallPropertyChangeHooks_() { - const nativeCb = this.nativeCb_; - const cbProto = Object.getPrototypeOf(nativeCb); - - CB_PROTO_PROPS.forEach((controlState) => { - const desc = Object.getOwnPropertyDescriptor(cbProto, controlState); - if (!validDescriptor(desc)) { - return; - } - Object.defineProperty(nativeCb, controlState, desc); - }); - } - - get ripple(): MDCRipple { - return this.ripple_; - } - - get checked(): boolean { - return this.nativeCb_.checked; - } - - set checked(checked: boolean) { - this.nativeCb_.checked = checked; - } - - get indeterminate(): boolean { - return this.nativeCb_.indeterminate; - } - - set indeterminate(indeterminate: boolean) { - this.nativeCb_.indeterminate = indeterminate; - } - - get disabled(): boolean { - return this.nativeCb_.disabled; - } - - set disabled(disabled: boolean) { - this.foundation_.setDisabled(disabled); - } - - get value(): string { - return this.nativeCb_.value; - } - - set value(value: string) { - this.nativeCb_.value = value; - } -} - -function validDescriptor(inputPropDesc: PropertyDescriptor | undefined): inputPropDesc is PropertyDescriptor { - return !!inputPropDesc && typeof inputPropDesc.set === 'function'; -} - -export {MDCCheckbox as default, MDCCheckbox}; export * from './adapter'; +export * from './component'; export * from './foundation'; diff --git a/packages/mdc-chips/chip-set/adapter.ts b/packages/mdc-chips/chip-set/adapter.ts index 4811400e7a4..6d4396fb178 100644 --- a/packages/mdc-chips/chip-set/adapter.ts +++ b/packages/mdc-chips/chip-set/adapter.ts @@ -28,7 +28,7 @@ * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCChipSetAdapter { +export interface MDCChipSetAdapter { /** * @return true if the root element contains the given class name. */ @@ -44,5 +44,3 @@ interface MDCChipSetAdapter { */ setSelected(chipId: string, selected: boolean): void; } - -export {MDCChipSetAdapter as default, MDCChipSetAdapter}; diff --git a/packages/mdc-chips/chip-set/component.ts b/packages/mdc-chips/chip-set/component.ts new file mode 100644 index 00000000000..6c9a84c92fd --- /dev/null +++ b/packages/mdc-chips/chip-set/component.ts @@ -0,0 +1,145 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {MDCChip, MDCChipFactory, MDCChipFoundation} from '../chip/index'; +import {MDCChipInteractionEvent, MDCChipRemovalEvent, MDCChipSelectionEvent} from '../chip/index'; +import {MDCChipSetAdapter} from './adapter'; +import {MDCChipSetFoundation} from './foundation'; + +const {INTERACTION_EVENT, SELECTION_EVENT, REMOVAL_EVENT} = MDCChipFoundation.strings; +const {CHIP_SELECTOR} = MDCChipSetFoundation.strings; + +let idCounter = 0; + +export class MDCChipSet extends MDCComponent { + static attachTo(root: Element) { + return new MDCChipSet(root); + } + + get chips(): ReadonlyArray { + return this.chips_.slice(); + } + + /** + * @return An array of the IDs of all selected chips. + */ + get selectedChipIds(): ReadonlyArray { + return this.foundation_.getSelectedChipIds(); + } + + private chips_!: MDCChip[]; // assigned in initialize() + private chipFactory_!: (el: Element) => MDCChip; // assigned in initialize() + private handleChipInteraction_!: (evt: MDCChipInteractionEvent) => void; // assigned in initialSyncWithDOM() + private handleChipSelection_!: (evt: MDCChipSelectionEvent) => void; // assigned in initialSyncWithDOM() + private handleChipRemoval_!: (evt: MDCChipRemovalEvent) => void; // assigned in initialSyncWithDOM() + + /** + * @param chipFactory A function which creates a new MDCChip. + */ + initialize(chipFactory: MDCChipFactory = (el) => new MDCChip(el)) { + this.chipFactory_ = chipFactory; + this.chips_ = this.instantiateChips_(this.chipFactory_); + } + + initialSyncWithDOM() { + this.chips_.forEach((chip) => { + if (chip.id && chip.selected) { + this.foundation_.select(chip.id); + } + }); + + this.handleChipInteraction_ = (evt) => this.foundation_.handleChipInteraction(evt.detail.chipId); + this.handleChipSelection_ = (evt) => this.foundation_.handleChipSelection(evt.detail.chipId, evt.detail.selected); + this.handleChipRemoval_ = (evt) => this.foundation_.handleChipRemoval(evt.detail.chipId); + this.listen(INTERACTION_EVENT, this.handleChipInteraction_); + this.listen(SELECTION_EVENT, this.handleChipSelection_); + this.listen(REMOVAL_EVENT, this.handleChipRemoval_); + } + + destroy() { + this.chips_.forEach((chip) => { + chip.destroy(); + }); + + this.unlisten(INTERACTION_EVENT, this.handleChipInteraction_); + this.unlisten(SELECTION_EVENT, this.handleChipSelection_); + this.unlisten(REMOVAL_EVENT, this.handleChipRemoval_); + + super.destroy(); + } + + /** + * Adds a new chip object to the chip set from the given chip element. + */ + addChip(chipEl: Element) { + chipEl.id = chipEl.id || `mdc-chip-${++idCounter}`; + this.chips_.push(this.chipFactory_(chipEl)); + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + const adapter: MDCChipSetAdapter = { + hasClass: (className) => this.root_.classList.contains(className), + removeChip: (chipId) => { + const index = this.findChipIndex_(chipId); + if (index >= 0) { + this.chips_[index].destroy(); + this.chips_.splice(index, 1); + } + }, + setSelected: (chipId, selected) => { + const index = this.findChipIndex_(chipId); + if (index >= 0) { + this.chips_[index].selected = selected; + } + }, + }; + return new MDCChipSetFoundation(adapter); + } + + /** + * Instantiates chip components on all of the chip set's child chip elements. + */ + private instantiateChips_(chipFactory: MDCChipFactory): MDCChip[] { + const chipElements: Element[] = + [].slice.call(this.root_.querySelectorAll(CHIP_SELECTOR)); + return chipElements.map((el) => { + el.id = el.id || `mdc-chip-${++idCounter}`; + return chipFactory(el); + }); + } + + /** + * Returns the index of the chip with the given id, or -1 if the chip does not exist. + */ + private findChipIndex_(chipId: string): number { + for (let i = 0; i < this.chips_.length; i++) { + if (this.chips_[i].id === chipId) { + return i; + } + } + return -1; + } +} diff --git a/packages/mdc-chips/chip-set/constants.ts b/packages/mdc-chips/chip-set/constants.ts index 99ff3f218d6..d4c67495e4e 100644 --- a/packages/mdc-chips/chip-set/constants.ts +++ b/packages/mdc-chips/chip-set/constants.ts @@ -21,13 +21,11 @@ * THE SOFTWARE. */ -const strings = { +export const strings = { CHIP_SELECTOR: '.mdc-chip', }; -const cssClasses = { +export const cssClasses = { CHOICE: 'mdc-chip-set--choice', FILTER: 'mdc-chip-set--filter', }; - -export {strings, cssClasses}; diff --git a/packages/mdc-chips/chip-set/foundation.ts b/packages/mdc-chips/chip-set/foundation.ts index b0106e386a9..7f2fa1373d0 100644 --- a/packages/mdc-chips/chip-set/foundation.ts +++ b/packages/mdc-chips/chip-set/foundation.ts @@ -25,7 +25,7 @@ import {MDCFoundation} from '@material/base/foundation'; import {MDCChipSetAdapter} from './adapter'; import {cssClasses, strings} from './constants'; -class MDCChipSetFoundation extends MDCFoundation { +export class MDCChipSetFoundation extends MDCFoundation { static get strings() { return strings; } @@ -127,4 +127,4 @@ class MDCChipSetFoundation extends MDCFoundation { } } -export {MDCChipSetFoundation as default, MDCChipSetFoundation}; +export default MDCChipSetFoundation; diff --git a/packages/mdc-chips/chip-set/index.ts b/packages/mdc-chips/chip-set/index.ts index 493df1354d6..f8c89ac94f3 100644 --- a/packages/mdc-chips/chip-set/index.ts +++ b/packages/mdc-chips/chip-set/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2016 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,122 +21,6 @@ * THE SOFTWARE. */ -import {MDCComponent} from '@material/base/component'; -import {MDCChip, MDCChipFoundation} from '../chip/index'; -import {MDCChipInteractionEvent, MDCChipRemovalEvent, MDCChipSelectionEvent} from '../chip/types'; -import {MDCChipSetFoundation} from './foundation'; - -type ChipFactory = (el: Element) => MDCChip; - -let idCounter = 0; - -class MDCChipSet extends MDCComponent { - /** - * @return An array of the IDs of all selected chips. - */ - get selectedChipIds(): ReadonlyArray { - return this.foundation_.getSelectedChipIds(); - } - - get chips(): ReadonlyArray { - return this.chips_.slice(); - } - - static attachTo(root: Element) { - return new MDCChipSet(root); - } - - private chips_!: MDCChip[]; // assigned in initialize() - private chipFactory_!: (el: Element) => MDCChip; // assigned in initialize() - private handleChipInteraction_!: (evt: MDCChipInteractionEvent) => void; // assigned in initialSyncWithDOM() - private handleChipSelection_!: (evt: MDCChipSelectionEvent) => void; // assigned in initialSyncWithDOM() - private handleChipRemoval_!: (evt: MDCChipRemovalEvent) => void; // assigned in initialSyncWithDOM() - - /** - * @param chipFactory A function which creates a new MDCChip. - */ - initialize(chipFactory: ChipFactory = (el) => new MDCChip(el)) { - this.chipFactory_ = chipFactory; - this.chips_ = this.instantiateChips_(this.chipFactory_); - } - - initialSyncWithDOM() { - this.chips_.forEach((chip) => { - if (chip.id && chip.selected) { - this.foundation_.select(chip.id); - } - }); - - this.handleChipInteraction_ = (evt) => this.foundation_.handleChipInteraction(evt.detail.chipId); - this.handleChipSelection_ = (evt) => this.foundation_.handleChipSelection(evt.detail.chipId, evt.detail.selected); - this.handleChipRemoval_ = (evt) => this.foundation_.handleChipRemoval(evt.detail.chipId); - this.listen(MDCChipFoundation.strings.INTERACTION_EVENT, this.handleChipInteraction_); - this.listen(MDCChipFoundation.strings.SELECTION_EVENT, this.handleChipSelection_); - this.listen(MDCChipFoundation.strings.REMOVAL_EVENT, this.handleChipRemoval_); - } - - destroy() { - this.chips_.forEach((chip) => { - chip.destroy(); - }); - - this.unlisten(MDCChipFoundation.strings.INTERACTION_EVENT, this.handleChipInteraction_); - this.unlisten(MDCChipFoundation.strings.SELECTION_EVENT, this.handleChipSelection_); - this.unlisten(MDCChipFoundation.strings.REMOVAL_EVENT, this.handleChipRemoval_); - - super.destroy(); - } - - /** - * Adds a new chip object to the chip set from the given chip element. - */ - addChip(chipEl: Element) { - chipEl.id = chipEl.id || `mdc-chip-${++idCounter}`; - this.chips_.push(this.chipFactory_(chipEl)); - } - - getDefaultFoundation() { - return new MDCChipSetFoundation({ - hasClass: (className) => this.root_.classList.contains(className), - removeChip: (chipId) => { - const index = this.findChipIndex_(chipId); - if (index >= 0) { - this.chips_[index].destroy(); - this.chips_.splice(index, 1); - } - }, - setSelected: (chipId, selected) => { - const index = this.findChipIndex_(chipId); - if (index >= 0) { - this.chips_[index].selected = selected; - } - }, - }); - } - - /** - * Instantiates chip components on all of the chip set's child chip elements. - */ - instantiateChips_(chipFactory: ChipFactory): MDCChip[] { - const chipElements: Element[] = - [].slice.call(this.root_.querySelectorAll(MDCChipSetFoundation.strings.CHIP_SELECTOR)); - return chipElements.map((el) => { - el.id = el.id || `mdc-chip-${++idCounter}`; - return chipFactory(el); - }); - } - - /** - * Returns the index of the chip with the given id, or -1 if the chip does not exist. - */ - findChipIndex_(chipId: string): number { - for (let i = 0; i < this.chips_.length; i++) { - if (this.chips_[i].id === chipId) { - return i; - } - } - return -1; - } -} - -export {MDCChipSet, MDCChipSetFoundation}; +export * from './adapter'; +export * from './component'; +export * from './foundation'; diff --git a/packages/mdc-chips/chip/adapter.ts b/packages/mdc-chips/chip/adapter.ts index 0cfe35b04d4..652a0ee3018 100644 --- a/packages/mdc-chips/chip/adapter.ts +++ b/packages/mdc-chips/chip/adapter.ts @@ -28,7 +28,7 @@ * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCChipAdapter { +export interface MDCChipAdapter { /** * Adds a class to the root element. */ @@ -106,5 +106,3 @@ interface MDCChipAdapter { */ getCheckmarkBoundingClientRect(): ClientRect | null; } - -export {MDCChipAdapter as default, MDCChipAdapter}; diff --git a/packages/mdc-chips/chip/component.ts b/packages/mdc-chips/chip/component.ts new file mode 100644 index 00000000000..91c4e0d18a9 --- /dev/null +++ b/packages/mdc-chips/chip/component.ts @@ -0,0 +1,187 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {SpecificEventListener} from '@material/base/types'; +import { + MDCRipple, + MDCRippleAdapter, + MDCRippleCapableSurface, + MDCRippleFactory, + MDCRippleFoundation, +} from '@material/ripple/index'; +import {MDCChipAdapter} from './adapter'; +import {strings} from './constants'; +import {MDCChipFoundation} from './foundation'; +import {MDCChipInteractionEventDetail, MDCChipRemovalEventDetail, MDCChipSelectionEventDetail} from './types'; + +type InteractionType = 'click' | 'keydown'; + +const INTERACTION_EVENTS: InteractionType[] = ['click', 'keydown']; + +export type MDCChipFactory = (el: Element, foundation?: MDCChipFoundation) => MDCChip; + +export class MDCChip extends MDCComponent implements MDCRippleCapableSurface { + /** + * @return Whether the chip is selected. + */ + get selected(): boolean { + return this.foundation_.isSelected(); + } + + /** + * Sets selected state on the chip. + */ + set selected(selected: boolean) { + this.foundation_.setSelected(selected); + } + + /** + * @return Whether a trailing icon click should trigger exit/removal of the chip. + */ + get shouldRemoveOnTrailingIconClick(): boolean { + return this.foundation_.getShouldRemoveOnTrailingIconClick(); + } + + /** + * Sets whether a trailing icon click should trigger exit/removal of the chip. + */ + set shouldRemoveOnTrailingIconClick(shouldRemove: boolean) { + this.foundation_.setShouldRemoveOnTrailingIconClick(shouldRemove); + } + + get ripple(): MDCRipple { + return this.ripple_; + } + + get id(): string { + return this.root_.id; + } + + static attachTo(root: Element) { + return new MDCChip(root); + } + + // Public visibility for this property is required by MDCRippleCapableSurface. + root_!: HTMLElement; // assigned in MDCComponent constructor + + private leadingIcon_!: Element | null; // assigned in initialize() + private trailingIcon_!: Element | null; // assigned in initialize() + private checkmark_!: Element | null; // assigned in initialize() + private ripple_!: MDCRipple; // assigned in initialize() + + private handleInteraction_!: SpecificEventListener; // assigned in initialSyncWithDOM() + private handleTransitionEnd_!: SpecificEventListener<'transitionend'>; // assigned in initialSyncWithDOM() + private handleTrailingIconInteraction_!: SpecificEventListener; // assigned in initialSyncWithDOM() + + initialize(rippleFactory: MDCRippleFactory = (el, foundation) => new MDCRipple(el, foundation)) { + this.leadingIcon_ = this.root_.querySelector(strings.LEADING_ICON_SELECTOR); + this.trailingIcon_ = this.root_.querySelector(strings.TRAILING_ICON_SELECTOR); + this.checkmark_ = this.root_.querySelector(strings.CHECKMARK_SELECTOR); + + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + const rippleAdapter: MDCRippleAdapter = { + ...MDCRipple.createAdapter(this), + computeBoundingRect: () => this.foundation_.getDimensions(), + }; + this.ripple_ = rippleFactory(this.root_, new MDCRippleFoundation(rippleAdapter)); + } + + initialSyncWithDOM() { + this.handleInteraction_ = (evt: MouseEvent | KeyboardEvent) => this.foundation_.handleInteraction(evt); + this.handleTransitionEnd_ = (evt: TransitionEvent) => this.foundation_.handleTransitionEnd(evt); + this.handleTrailingIconInteraction_ = (evt: MouseEvent | KeyboardEvent) => + this.foundation_.handleTrailingIconInteraction(evt); + + INTERACTION_EVENTS.forEach((evtType) => { + this.listen(evtType, this.handleInteraction_); + }); + this.listen('transitionend', this.handleTransitionEnd_); + + if (this.trailingIcon_) { + INTERACTION_EVENTS.forEach((evtType) => { + this.trailingIcon_!.addEventListener(evtType, this.handleTrailingIconInteraction_ as EventListener); + }); + } + } + + destroy() { + this.ripple_.destroy(); + + INTERACTION_EVENTS.forEach((evtType) => { + this.unlisten(evtType, this.handleInteraction_); + }); + this.unlisten('transitionend', this.handleTransitionEnd_); + + if (this.trailingIcon_) { + INTERACTION_EVENTS.forEach((evtType) => { + this.trailingIcon_!.removeEventListener(evtType, this.handleTrailingIconInteraction_ as EventListener); + }); + } + + super.destroy(); + } + + /** + * Begins the exit animation which leads to removal of the chip. + */ + beginExit() { + this.foundation_.beginExit(); + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + const adapter: MDCChipAdapter = { + addClass: (className) => this.root_.classList.add(className), + addClassToLeadingIcon: (className) => { + if (this.leadingIcon_) { + this.leadingIcon_.classList.add(className); + } + }, + eventTargetHasClass: (target, className) => target ? (target as Element).classList.contains(className) : false, + getCheckmarkBoundingClientRect: () => this.checkmark_ ? this.checkmark_.getBoundingClientRect() : null, + getComputedStyleValue: (propertyName) => window.getComputedStyle(this.root_).getPropertyValue(propertyName), + getRootBoundingClientRect: () => this.root_.getBoundingClientRect(), + hasClass: (className) => this.root_.classList.contains(className), + hasLeadingIcon: () => !!this.leadingIcon_, + notifyInteraction: () => this.emit( + strings.INTERACTION_EVENT, {chipId: this.id}, true /* shouldBubble */), + notifyRemoval: () => this.emit( + strings.REMOVAL_EVENT, {chipId: this.id, root: this.root_}, true /* shouldBubble */), + notifySelection: (selected) => this.emit( + strings.SELECTION_EVENT, {chipId: this.id, selected}, true /* shouldBubble */), + notifyTrailingIconInteraction: () => this.emit( + strings.TRAILING_ICON_INTERACTION_EVENT, {chipId: this.id}, true /* shouldBubble */), + removeClass: (className) => this.root_.classList.remove(className), + removeClassFromLeadingIcon: (className) => { + if (this.leadingIcon_) { + this.leadingIcon_.classList.remove(className); + } + }, + setStyleProperty: (propertyName, value) => this.root_.style.setProperty(propertyName, value), + }; + return new MDCChipFoundation(adapter); + } +} diff --git a/packages/mdc-chips/chip/constants.ts b/packages/mdc-chips/chip/constants.ts index 111cdaa2aa0..68df5f68420 100644 --- a/packages/mdc-chips/chip/constants.ts +++ b/packages/mdc-chips/chip/constants.ts @@ -21,7 +21,7 @@ * THE SOFTWARE. */ -const strings = { +export const strings = { CHECKMARK_SELECTOR: '.mdc-chip__checkmark', ENTRY_ANIMATION_NAME: 'mdc-chip-entry', INTERACTION_EVENT: 'MDCChip:interaction', @@ -32,7 +32,7 @@ const strings = { TRAILING_ICON_SELECTOR: '.mdc-chip__icon--trailing', }; -const cssClasses = { +export const cssClasses = { CHECKMARK: 'mdc-chip__checkmark', CHIP_EXIT: 'mdc-chip--exit', HIDDEN_LEADING_ICON: 'mdc-chip__icon--leading-hidden', @@ -40,5 +40,3 @@ const cssClasses = { SELECTED: 'mdc-chip--selected', TRAILING_ICON: 'mdc-chip__icon--trailing', }; - -export {cssClasses, strings}; diff --git a/packages/mdc-chips/chip/foundation.ts b/packages/mdc-chips/chip/foundation.ts index 6d4e2e905c8..4d667bd3418 100644 --- a/packages/mdc-chips/chip/foundation.ts +++ b/packages/mdc-chips/chip/foundation.ts @@ -34,7 +34,7 @@ const emptyClientRect = { width: 0, }; -class MDCChipFoundation extends MDCFoundation { +export class MDCChipFoundation extends MDCFoundation { static get strings() { return strings; } @@ -185,4 +185,4 @@ class MDCChipFoundation extends MDCFoundation { } } -export {MDCChipFoundation as default, MDCChipFoundation}; +export default MDCChipFoundation; diff --git a/packages/mdc-chips/chip/index.ts b/packages/mdc-chips/chip/index.ts index 26239c5c258..37eb9683f7f 100644 --- a/packages/mdc-chips/chip/index.ts +++ b/packages/mdc-chips/chip/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2016 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,159 +21,7 @@ * THE SOFTWARE. */ -import {MDCComponent} from '@material/base/component'; -import {SpecificEventListener} from '@material/base/index'; -import {MDCRipple, MDCRippleFoundation, RippleCapableSurface} from '@material/ripple/index'; -import {strings} from './constants'; -import {MDCChipFoundation} from './foundation'; -import { - MDCChipInteractionEventDetail, - MDCChipRemovalEventDetail, - MDCChipSelectionEventDetail, - RippleFactory, -} from './types'; - -type InteractionType = 'click' | 'keydown'; - -const INTERACTION_EVENTS: InteractionType[] = ['click', 'keydown']; - -class MDCChip extends MDCComponent implements RippleCapableSurface { - /** - * @return Whether the chip is selected. - */ - get selected(): boolean { - return this.foundation_.isSelected(); - } - - /** - * Sets selected state on the chip. - */ - set selected(selected: boolean) { - this.foundation_.setSelected(selected); - } - - /** - * @return Whether a trailing icon click should trigger exit/removal of the chip. - */ - get shouldRemoveOnTrailingIconClick(): boolean { - return this.foundation_.getShouldRemoveOnTrailingIconClick(); - } - - /** - * Sets whether a trailing icon click should trigger exit/removal of the chip. - */ - set shouldRemoveOnTrailingIconClick(shouldRemove: boolean) { - this.foundation_.setShouldRemoveOnTrailingIconClick(shouldRemove); - } - - get ripple(): MDCRipple { - return this.ripple_; - } - - get id(): string { - return this.root_.id; - } - - static attachTo(root: Element) { - return new MDCChip(root); - } - - // Public visibility for this property is required by RippleCapableSurface. - root_!: HTMLElement; // assigned in MDCComponent constructor - - private leadingIcon_!: Element | null; // assigned in initialize() - private trailingIcon_!: Element | null; // assigned in initialize() - private checkmark_!: Element | null; // assigned in initialize() - private ripple_!: MDCRipple; // assigned in initialize() - - private handleInteraction_!: SpecificEventListener; // assigned in initialSyncWithDOM() - private handleTransitionEnd_!: SpecificEventListener<'transitionend'>; // assigned in initialSyncWithDOM() - private handleTrailingIconInteraction_!: SpecificEventListener; // assigned in initialSyncWithDOM() - - initialize(rippleFactory: RippleFactory = (el, foundation) => new MDCRipple(el, foundation)) { - this.leadingIcon_ = this.root_.querySelector(strings.LEADING_ICON_SELECTOR); - this.trailingIcon_ = this.root_.querySelector(strings.TRAILING_ICON_SELECTOR); - this.checkmark_ = this.root_.querySelector(strings.CHECKMARK_SELECTOR); - - this.ripple_ = rippleFactory(this.root_, new MDCRippleFoundation({ - ...MDCRipple.createAdapter(this), - computeBoundingRect: () => this.foundation_.getDimensions(), - })); - } - - initialSyncWithDOM() { - this.handleInteraction_ = (evt: MouseEvent | KeyboardEvent) => this.foundation_.handleInteraction(evt); - this.handleTransitionEnd_ = (evt: TransitionEvent) => this.foundation_.handleTransitionEnd(evt); - this.handleTrailingIconInteraction_ = (evt: MouseEvent | KeyboardEvent) => - this.foundation_.handleTrailingIconInteraction(evt); - - INTERACTION_EVENTS.forEach((evtType) => { - this.listen(evtType, this.handleInteraction_); - }); - this.listen('transitionend', this.handleTransitionEnd_); - - if (this.trailingIcon_) { - INTERACTION_EVENTS.forEach((evtType) => { - this.trailingIcon_!.addEventListener(evtType, this.handleTrailingIconInteraction_ as EventListener); - }); - } - } - - destroy() { - this.ripple_.destroy(); - - INTERACTION_EVENTS.forEach((evtType) => { - this.unlisten(evtType, this.handleInteraction_); - }); - this.unlisten('transitionend', this.handleTransitionEnd_); - - if (this.trailingIcon_) { - INTERACTION_EVENTS.forEach((evtType) => { - this.trailingIcon_!.removeEventListener(evtType, this.handleTrailingIconInteraction_ as EventListener); - }); - } - - super.destroy(); - } - - /** - * Begins the exit animation which leads to removal of the chip. - */ - beginExit() { - this.foundation_.beginExit(); - } - - getDefaultFoundation() { - return new MDCChipFoundation({ - addClass: (className) => this.root_.classList.add(className), - addClassToLeadingIcon: (className) => { - if (this.leadingIcon_) { - this.leadingIcon_.classList.add(className); - } - }, - eventTargetHasClass: (target, className) => target ? (target as Element).classList.contains(className) : false, - getCheckmarkBoundingClientRect: () => this.checkmark_ ? this.checkmark_.getBoundingClientRect() : null, - getComputedStyleValue: (propertyName) => window.getComputedStyle(this.root_).getPropertyValue(propertyName), - getRootBoundingClientRect: () => this.root_.getBoundingClientRect(), - hasClass: (className) => this.root_.classList.contains(className), - hasLeadingIcon: () => !!this.leadingIcon_, - notifyInteraction: () => this.emit( - strings.INTERACTION_EVENT, {chipId: this.id}, true /* shouldBubble */), - notifyRemoval: () => this.emit( - strings.REMOVAL_EVENT, {chipId: this.id, root: this.root_}, true /* shouldBubble */), - notifySelection: (selected) => this.emit( - strings.SELECTION_EVENT, {chipId: this.id, selected}, true /* shouldBubble */), - notifyTrailingIconInteraction: () => this.emit( - strings.TRAILING_ICON_INTERACTION_EVENT, {chipId: this.id}, true /* shouldBubble */), - removeClass: (className) => this.root_.classList.remove(className), - removeClassFromLeadingIcon: (className) => { - if (this.leadingIcon_) { - this.leadingIcon_.classList.remove(className); - } - }, - setStyleProperty: (propertyName, value) => this.root_.style.setProperty(propertyName, value), - }); - } -} - -export {MDCChip, MDCChipFoundation}; +export * from './adapter'; +export * from './component'; +export * from './foundation'; +export * from './types'; diff --git a/packages/mdc-chips/chip/types.ts b/packages/mdc-chips/chip/types.ts index cf2674f2ca7..10043689815 100644 --- a/packages/mdc-chips/chip/types.ts +++ b/packages/mdc-chips/chip/types.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2016 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,8 +21,6 @@ * THE SOFTWARE. */ -import {MDCRipple, MDCRippleFoundation} from '@material/ripple/index'; - export interface MDCChipInteractionEventDetail { chipId: string; } @@ -35,8 +33,16 @@ export interface MDCChipRemovalEventDetail extends MDCChipInteractionEventDetail root: Element; } -export interface MDCChipInteractionEvent extends CustomEvent {} -export interface MDCChipSelectionEvent extends CustomEvent {} -export interface MDCChipRemovalEvent extends CustomEvent {} +// Note: CustomEvent is not supported by Closure Compiler. + +export interface MDCChipInteractionEvent extends Event { + readonly detail: MDCChipInteractionEventDetail; +} + +export interface MDCChipSelectionEvent extends Event { + readonly detail: MDCChipSelectionEventDetail; +} -export type RippleFactory = (el: Element, foundation: MDCRippleFoundation) => MDCRipple; +export interface MDCChipRemovalEvent extends Event { + readonly detail: MDCChipRemovalEventDetail; +} diff --git a/packages/mdc-chips/index.ts b/packages/mdc-chips/index.ts index d4151ec130b..928f4eba102 100644 --- a/packages/mdc-chips/index.ts +++ b/packages/mdc-chips/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,10 +21,5 @@ * THE SOFTWARE. */ -export * from './chip/adapter'; -export * from './chip/foundation'; export * from './chip/index'; -export * from './chip/types'; -export * from './chip-set/adapter'; -export * from './chip-set/foundation'; export * from './chip-set/index'; diff --git a/packages/mdc-dialog/README.md b/packages/mdc-dialog/README.md index 378797903f3..392428f801f 100644 --- a/packages/mdc-dialog/README.md +++ b/packages/mdc-dialog/README.md @@ -89,8 +89,9 @@ const dialog = new MDCDialog(document.querySelector('.mdc-dialog')); > See [Importing the JS component](../../docs/importing-js.md) for more information on how to import JavaScript. MDC Dialog makes no assumptions about what will be added to the `mdc-dialog__content` element. Any List, Checkboxes, -etc. must also be instantiated. Additionally, call `layout` on any applicable components within the content when -`MDCDialog:opened` is emitted. +etc. must also be instantiated. If your dialog contains any layout-sensitive components, you should wait until +`MDCDialog:opened` is emitted to instantiate them (or call `layout` on them) so that the dialog's transition finishes +first. For example, to instantiate an MDC List inside of a Simple or Confirmation Dialog: @@ -103,6 +104,9 @@ dialog.listen('MDCDialog:opened', () => { }); ``` +> *NOTE*: Mispositioned or incorrectly-sized elements (e.g. ripples, floating labels, notched outlines) are a strong +> indication that child components are being instantiated before the dialog has finished opening. + ## Variants ### Simple Dialog diff --git a/packages/mdc-dialog/adapter.ts b/packages/mdc-dialog/adapter.ts index fcb5be36963..880d2f5bbcf 100644 --- a/packages/mdc-dialog/adapter.ts +++ b/packages/mdc-dialog/adapter.ts @@ -28,7 +28,7 @@ * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCDialogAdapter { +export interface MDCDialogAdapter { addClass(className: string): void; removeClass(className: string): void; hasClass(className: string): boolean; @@ -50,5 +50,3 @@ interface MDCDialogAdapter { notifyClosing(action: string): void; notifyClosed(action: string): void; } - -export {MDCDialogAdapter as default, MDCDialogAdapter}; diff --git a/packages/mdc-dialog/component.ts b/packages/mdc-dialog/component.ts new file mode 100644 index 00000000000..fbd68e89d01 --- /dev/null +++ b/packages/mdc-dialog/component.ts @@ -0,0 +1,188 @@ +/** + * @license + * Copyright 2017 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {SpecificEventListener} from '@material/base/types'; +import {closest, matches} from '@material/dom/ponyfill'; +import {MDCRipple} from '@material/ripple/component'; +import * as FocusTrapLib from 'focus-trap'; +import {MDCDialogAdapter} from './adapter'; +import {MDCDialogFoundation} from './foundation'; +import {MDCDialogCloseEventDetail} from './types'; +import * as util from './util'; +import {MDCDialogFocusTrapFactory} from './util'; + +const {strings} = MDCDialogFoundation; + +export class MDCDialog extends MDCComponent { + get isOpen() { + return this.foundation_.isOpen(); + } + + get escapeKeyAction() { + return this.foundation_.getEscapeKeyAction(); + } + + set escapeKeyAction(action) { + this.foundation_.setEscapeKeyAction(action); + } + + get scrimClickAction() { + return this.foundation_.getScrimClickAction(); + } + + set scrimClickAction(action) { + this.foundation_.setScrimClickAction(action); + } + + get autoStackButtons() { + return this.foundation_.getAutoStackButtons(); + } + + set autoStackButtons(autoStack) { + this.foundation_.setAutoStackButtons(autoStack); + } + + static attachTo(root: Element) { + return new MDCDialog(root); + } + + private buttonRipples_!: MDCRipple[]; // assigned in initialize() + private buttons_!: HTMLElement[]; // assigned in initialize() + private container_!: HTMLElement; // assigned in initialize() + private content_!: HTMLElement | null; // assigned in initialize() + private defaultButton_!: HTMLElement | null; // assigned in initialize() + private initialFocusEl_?: HTMLElement; // assigned in initialize() + + private focusTrap_!: FocusTrapLib.FocusTrap; // assigned in initialSyncWithDOM() + private focusTrapFactory_?: MDCDialogFocusTrapFactory; // assigned in initialize() + + private handleInteraction_!: SpecificEventListener<'click' | 'keydown'>; // assigned in initialSyncWithDOM() + private handleDocumentKeydown_!: SpecificEventListener<'keydown'>; // assigned in initialSyncWithDOM() + private handleLayout_!: EventListener; // assigned in initialSyncWithDOM() + private handleOpening_!: EventListener; // assigned in initialSyncWithDOM() + private handleClosing_!: () => void; // assigned in initialSyncWithDOM() + + initialize( + focusTrapFactory?: MDCDialogFocusTrapFactory, + initialFocusEl?: HTMLElement, + ) { + const container = this.root_.querySelector(strings.CONTAINER_SELECTOR); + if (!container) { + throw new Error(`Dialog component requires a ${strings.CONTAINER_SELECTOR} container element`); + } + this.container_ = container; + this.content_ = this.root_.querySelector(strings.CONTENT_SELECTOR); + this.buttons_ = [].slice.call(this.root_.querySelectorAll(strings.BUTTON_SELECTOR)); + this.defaultButton_ = this.root_.querySelector(strings.DEFAULT_BUTTON_SELECTOR); + this.focusTrapFactory_ = focusTrapFactory; + this.initialFocusEl_ = initialFocusEl; + this.buttonRipples_ = []; + + for (const buttonEl of this.buttons_) { + this.buttonRipples_.push(new MDCRipple(buttonEl)); + } + } + + initialSyncWithDOM() { + this.focusTrap_ = util.createFocusTrapInstance(this.container_, this.focusTrapFactory_, this.initialFocusEl_); + + this.handleInteraction_ = this.foundation_.handleInteraction.bind(this.foundation_); + this.handleDocumentKeydown_ = this.foundation_.handleDocumentKeydown.bind(this.foundation_); + this.handleLayout_ = this.layout.bind(this); + + const LAYOUT_EVENTS = ['resize', 'orientationchange']; + this.handleOpening_ = () => { + LAYOUT_EVENTS.forEach((evtType) => window.addEventListener(evtType, this.handleLayout_)); + document.addEventListener('keydown', this.handleDocumentKeydown_); + }; + this.handleClosing_ = () => { + LAYOUT_EVENTS.forEach((evtType) => window.removeEventListener(evtType, this.handleLayout_)); + document.removeEventListener('keydown', this.handleDocumentKeydown_); + }; + + this.listen('click', this.handleInteraction_); + this.listen('keydown', this.handleInteraction_); + this.listen(strings.OPENING_EVENT, this.handleOpening_); + this.listen(strings.CLOSING_EVENT, this.handleClosing_); + } + + destroy() { + this.unlisten('click', this.handleInteraction_); + this.unlisten('keydown', this.handleInteraction_); + this.unlisten(strings.OPENING_EVENT, this.handleOpening_); + this.unlisten(strings.CLOSING_EVENT, this.handleClosing_); + this.handleClosing_(); + + this.buttonRipples_.forEach((ripple) => ripple.destroy()); + super.destroy(); + } + + layout() { + this.foundation_.layout(); + } + + open() { + this.foundation_.open(); + } + + close(action = '') { + this.foundation_.close(action); + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + const adapter: MDCDialogAdapter = { + addBodyClass: (className) => document.body.classList.add(className), + addClass: (className) => this.root_.classList.add(className), + areButtonsStacked: () => util.areTopsMisaligned(this.buttons_), + clickDefaultButton: () => this.defaultButton_ && this.defaultButton_.click(), + eventTargetMatches: (target, selector) => target ? matches(target as Element, selector) : false, + getActionFromEvent: (evt: Event) => { + if (!evt.target) { + return ''; + } + const element = closest(evt.target as Element, `[${strings.ACTION_ATTRIBUTE}]`); + return element && element.getAttribute(strings.ACTION_ATTRIBUTE); + }, + hasClass: (className) => this.root_.classList.contains(className), + isContentScrollable: () => util.isScrollable(this.content_), + notifyClosed: (action) => this.emit(strings.CLOSED_EVENT, action ? {action} : {}), + notifyClosing: (action) => this.emit(strings.CLOSING_EVENT, action ? {action} : {}), + notifyOpened: () => this.emit(strings.OPENED_EVENT, {}), + notifyOpening: () => this.emit(strings.OPENING_EVENT, {}), + releaseFocus: () => this.focusTrap_.deactivate(), + removeBodyClass: (className) => document.body.classList.remove(className), + removeClass: (className) => this.root_.classList.remove(className), + reverseButtons: () => { + this.buttons_.reverse(); + this.buttons_.forEach((button) => { + button.parentElement!.appendChild(button); + }); + }, + trapFocus: () => this.focusTrap_.activate(), + }; + return new MDCDialogFoundation(adapter); + } +} diff --git a/packages/mdc-dialog/constants.ts b/packages/mdc-dialog/constants.ts index b2be89d2153..122a7073424 100644 --- a/packages/mdc-dialog/constants.ts +++ b/packages/mdc-dialog/constants.ts @@ -21,7 +21,7 @@ * THE SOFTWARE. */ -const cssClasses = { +export const cssClasses = { CLOSING: 'mdc-dialog--closing', OPEN: 'mdc-dialog--open', OPENING: 'mdc-dialog--opening', @@ -30,7 +30,7 @@ const cssClasses = { STACKED: 'mdc-dialog--stacked', }; -const strings = { +export const strings = { ACTION_ATTRIBUTE: 'data-mdc-dialog-action', BUTTON_SELECTOR: '.mdc-dialog__button', CLOSED_EVENT: 'MDCDialog:closed', @@ -50,9 +50,7 @@ const strings = { SURFACE_SELECTOR: '.mdc-dialog__surface', }; -const numbers = { +export const numbers = { DIALOG_ANIMATION_CLOSE_TIME_MS: 75, DIALOG_ANIMATION_OPEN_TIME_MS: 150, }; - -export {cssClasses, numbers, strings}; diff --git a/packages/mdc-dialog/foundation.ts b/packages/mdc-dialog/foundation.ts index db36b5e5765..56f22baa51f 100644 --- a/packages/mdc-dialog/foundation.ts +++ b/packages/mdc-dialog/foundation.ts @@ -25,7 +25,7 @@ import {MDCFoundation} from '@material/base/foundation'; import {MDCDialogAdapter} from './adapter'; import {cssClasses, numbers, strings} from './constants'; -class MDCDialogFoundation extends MDCFoundation { +export class MDCDialogFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } @@ -253,4 +253,4 @@ class MDCDialogFoundation extends MDCFoundation { } } -export {MDCDialogFoundation as default, MDCDialogFoundation}; +export default MDCDialogFoundation; diff --git a/packages/mdc-dialog/index.ts b/packages/mdc-dialog/index.ts index dce700fb22f..3de836c29e1 100644 --- a/packages/mdc-dialog/index.ts +++ b/packages/mdc-dialog/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,167 +21,10 @@ * THE SOFTWARE. */ -import {MDCComponent} from '@material/base/component'; -import {SpecificEventListener} from '@material/base/index'; -import {closest, matches} from '@material/dom/ponyfill'; -import {MDCRipple} from '@material/ripple/index'; -import * as createFocusTrap from 'focus-trap'; -import MDCDialogFoundation from './foundation'; -import {FocusTrapFactory} from './types'; import * as util from './util'; -const strings = MDCDialogFoundation.strings; - -class MDCDialog extends MDCComponent { - get isOpen() { - return this.foundation_.isOpen(); - } - - get escapeKeyAction() { - return this.foundation_.getEscapeKeyAction(); - } - - set escapeKeyAction(action) { - this.foundation_.setEscapeKeyAction(action); - } - - get scrimClickAction() { - return this.foundation_.getScrimClickAction(); - } - - set scrimClickAction(action) { - this.foundation_.setScrimClickAction(action); - } - - get autoStackButtons() { - return this.foundation_.getAutoStackButtons(); - } - - set autoStackButtons(autoStack) { - this.foundation_.setAutoStackButtons(autoStack); - } - - static attachTo(root: Element) { - return new MDCDialog(root); - } - - private buttonRipples_!: MDCRipple[]; // assigned in initialize() - private buttons_!: HTMLElement[]; // assigned in initialize() - private container_!: HTMLElement; // assigned in initialize() - private content_!: HTMLElement | null; // assigned in initialize() - private defaultButton_!: HTMLElement | null; // assigned in initialize() - private initialFocusEl_?: HTMLElement; // assigned in initialize() - - private focusTrap_!: createFocusTrap.FocusTrap; // assigned in initialSyncWithDOM() - private focusTrapFactory_!: FocusTrapFactory; // assigned in initialize() - - private handleInteraction_!: SpecificEventListener<'click'|'keydown'>; // assigned in initialSyncWithDOM() - private handleDocumentKeydown_!: SpecificEventListener<'keydown'>; // assigned in initialSyncWithDOM() - private handleLayout_!: EventListener; // assigned in initialSyncWithDOM() - private handleOpening_!: EventListener; // assigned in initialSyncWithDOM() - private handleClosing_!: () => void; // assigned in initialSyncWithDOM() - - initialize( - focusTrapFactory: FocusTrapFactory = createFocusTrap as unknown as FocusTrapFactory, - initialFocusEl?: HTMLElement) { - const container = this.root_.querySelector(strings.CONTAINER_SELECTOR); - if (!container) { - throw new Error(`Dialog component requires a ${strings.CONTAINER_SELECTOR} container element`); - } - this.container_ = container; - this.content_ = this.root_.querySelector(strings.CONTENT_SELECTOR); - this.buttons_ = [].slice.call(this.root_.querySelectorAll(strings.BUTTON_SELECTOR)); - this.defaultButton_ = this.root_.querySelector(strings.DEFAULT_BUTTON_SELECTOR); - this.focusTrapFactory_ = focusTrapFactory; - this.initialFocusEl_ = initialFocusEl; - this.buttonRipples_ = []; - - for (const buttonEl of this.buttons_) { - this.buttonRipples_.push(new MDCRipple(buttonEl)); - } - } - - initialSyncWithDOM() { - this.focusTrap_ = util.createFocusTrapInstance(this.container_, this.focusTrapFactory_, this.initialFocusEl_); - - this.handleInteraction_ = this.foundation_.handleInteraction.bind(this.foundation_); - this.handleDocumentKeydown_ = this.foundation_.handleDocumentKeydown.bind(this.foundation_); - this.handleLayout_ = this.layout.bind(this); - - const LAYOUT_EVENTS = ['resize', 'orientationchange']; - this.handleOpening_ = () => { - LAYOUT_EVENTS.forEach((evtType) => window.addEventListener(evtType, this.handleLayout_)); - document.addEventListener('keydown', this.handleDocumentKeydown_); - }; - this.handleClosing_ = () => { - LAYOUT_EVENTS.forEach((evtType) => window.removeEventListener(evtType, this.handleLayout_)); - document.removeEventListener('keydown', this.handleDocumentKeydown_); - }; - - this.listen('click', this.handleInteraction_); - this.listen('keydown', this.handleInteraction_); - this.listen(strings.OPENING_EVENT, this.handleOpening_); - this.listen(strings.CLOSING_EVENT, this.handleClosing_); - } - - destroy() { - this.unlisten('click', this.handleInteraction_); - this.unlisten('keydown', this.handleInteraction_); - this.unlisten(strings.OPENING_EVENT, this.handleOpening_); - this.unlisten(strings.CLOSING_EVENT, this.handleClosing_); - this.handleClosing_(); - - this.buttonRipples_.forEach((ripple) => ripple.destroy()); - super.destroy(); - } - - layout() { - this.foundation_.layout(); - } - - open() { - this.foundation_.open(); - } - - close(action = '') { - this.foundation_.close(action); - } - - getDefaultFoundation() { - return new MDCDialogFoundation({ - addBodyClass: (className) => document.body.classList.add(className), - addClass: (className) => this.root_.classList.add(className), - areButtonsStacked: () => util.areTopsMisaligned(this.buttons_), - clickDefaultButton: () => this.defaultButton_ && this.defaultButton_.click(), - eventTargetMatches: (target, selector) => target ? matches(target as Element, selector) : false, - getActionFromEvent: (evt: Event) => { - if (!evt.target) { - return ''; - } - const element = closest(evt.target as Element, `[${strings.ACTION_ATTRIBUTE}]`); - return element && element.getAttribute(strings.ACTION_ATTRIBUTE); - }, - hasClass: (className) => this.root_.classList.contains(className), - isContentScrollable: () => util.isScrollable(this.content_), - notifyClosed: (action) => this.emit(strings.CLOSED_EVENT, action ? {action} : {}), - notifyClosing: (action) => this.emit(strings.CLOSING_EVENT, action ? {action} : {}), - notifyOpened: () => this.emit(strings.OPENED_EVENT, {}), - notifyOpening: () => this.emit(strings.OPENING_EVENT, {}), - releaseFocus: () => this.focusTrap_.deactivate(), - removeBodyClass: (className) => document.body.classList.remove(className), - removeClass: (className) => this.root_.classList.remove(className), - reverseButtons: () => { - this.buttons_.reverse(); - this.buttons_.forEach((button) => { - button.parentElement!.appendChild(button); - }); - }, - trapFocus: () => this.focusTrap_.activate(), - }); - } -} - -export {MDCDialog as default, MDCDialog, util}; +export {util}; export * from './adapter'; +export * from './component'; export * from './foundation'; export * from './types'; diff --git a/packages/mdc-dialog/types.ts b/packages/mdc-dialog/types.ts index 8121e8e1665..b352a808dcb 100644 --- a/packages/mdc-dialog/types.ts +++ b/packages/mdc-dialog/types.ts @@ -21,10 +21,12 @@ * THE SOFTWARE. */ -import * as FocusTrapLib from 'focus-trap'; +export interface MDCDialogCloseEventDetail { + action?: string; +} -// TODO(acdvorak): Centralize this in mdc-base or mdc-dom? -export type FocusTrapFactory = ( - element: HTMLElement | string, - userOptions?: FocusTrapLib.Options, -) => FocusTrapLib.FocusTrap; +// Note: CustomEvent is not supported by Closure Compiler. + +export interface MDCDialogCloseEvent extends Event { + readonly detail: MDCDialogCloseEventDetail; +} diff --git a/packages/mdc-dialog/util.ts b/packages/mdc-dialog/util.ts index 1f332a28216..dde25efcc45 100644 --- a/packages/mdc-dialog/util.ts +++ b/packages/mdc-dialog/util.ts @@ -22,11 +22,15 @@ */ import * as createFocusTrap from 'focus-trap'; -import {FocusTrapFactory} from './types'; + +export type MDCDialogFocusTrapFactory = ( + element: HTMLElement | string, + userOptions?: createFocusTrap.Options, +) => createFocusTrap.FocusTrap; export function createFocusTrapInstance( surfaceEl: HTMLElement, - focusTrapFactory: FocusTrapFactory = createFocusTrap as unknown as FocusTrapFactory, + focusTrapFactory: MDCDialogFocusTrapFactory = createFocusTrap as unknown as MDCDialogFocusTrapFactory, initialFocusEl?: createFocusTrap.FocusTarget, ): createFocusTrap.FocusTrap { return focusTrapFactory(surfaceEl, { diff --git a/packages/mdc-dom/ponyfill.ts b/packages/mdc-dom/ponyfill.ts index 006c83af7bd..a09d5808495 100644 --- a/packages/mdc-dom/ponyfill.ts +++ b/packages/mdc-dom/ponyfill.ts @@ -26,8 +26,8 @@ * This makes ponyfills safer than traditional polyfills, especially for libraries like MDC. */ -function closest(element: Element, selector: string): T | null; -function closest(element: Element, selector: string): Element | null { +export function closest(element: Element, selector: string): T | null; +export function closest(element: Element, selector: string): Element | null { if (element.closest) { return element.closest(selector); } @@ -42,11 +42,9 @@ function closest(element: Element, selector: string): Element | null { return null; } -function matches(element: Element, selector: string): boolean { +export function matches(element: Element, selector: string): boolean { const nativeMatches = element.matches || element.webkitMatchesSelector || element.msMatchesSelector; return nativeMatches.call(element, selector); } - -export {closest, matches}; diff --git a/packages/mdc-drawer/adapter.ts b/packages/mdc-drawer/adapter.ts index 8448135b34c..91c5f0add42 100644 --- a/packages/mdc-drawer/adapter.ts +++ b/packages/mdc-drawer/adapter.ts @@ -28,7 +28,7 @@ * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCDrawerAdapter { +export interface MDCDrawerAdapter { /** * Adds a class to the root Element. */ @@ -86,5 +86,3 @@ interface MDCDrawerAdapter { */ releaseFocus(): void; } - -export {MDCDrawerAdapter as default, MDCDrawerAdapter}; diff --git a/packages/mdc-drawer/component.ts b/packages/mdc-drawer/component.ts new file mode 100644 index 00000000000..2cefdefd3b2 --- /dev/null +++ b/packages/mdc-drawer/component.ts @@ -0,0 +1,158 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {SpecificEventListener} from '@material/base/types'; +import {MDCList, MDCListFactory} from '@material/list/component'; +import {MDCListFoundation} from '@material/list/foundation'; +import * as createFocusTrap from 'focus-trap'; +import {MDCDrawerAdapter} from './adapter'; +import {MDCDismissibleDrawerFoundation} from './dismissible/foundation'; +import {MDCModalDrawerFoundation} from './modal/foundation'; +import * as util from './util'; +import {MDCDrawerFocusTrapFactory} from './util'; + +const {cssClasses, strings} = MDCDismissibleDrawerFoundation; + +export class MDCDrawer extends MDCComponent { + static attachTo(root: Element): MDCDrawer { + return new MDCDrawer(root); + } + + /** + * Returns true if drawer is in the open position. + */ + get open(): boolean { + return this.foundation_.isOpen(); + } + + /** + * Toggles the drawer open and closed. + */ + set open(isOpen: boolean) { + if (isOpen) { + this.foundation_.open(); + } else { + this.foundation_.close(); + } + } + + private previousFocus_?: Element | null; + private scrim_!: HTMLElement | null; // assigned in initialSyncWithDOM() + private list_?: MDCList; // assigned in initialize() + + private focusTrap_?: createFocusTrap.FocusTrap; // assigned in initialSyncWithDOM() + private focusTrapFactory_!: MDCDrawerFocusTrapFactory; // assigned in initialize() + + private handleScrimClick_?: SpecificEventListener<'click'>; // initialized in initialSyncWithDOM() + private handleKeydown_!: SpecificEventListener<'keydown'>; // initialized in initialSyncWithDOM() + private handleTransitionEnd_!: SpecificEventListener<'transitionend'>; // initialized in initialSyncWithDOM() + + initialize( + focusTrapFactory: MDCDrawerFocusTrapFactory = createFocusTrap as unknown as MDCDrawerFocusTrapFactory, + listFactory: MDCListFactory = (el) => new MDCList(el), + ) { + const listEl = this.root_.querySelector(`.${MDCListFoundation.cssClasses.ROOT}`); + if (listEl) { + this.list_ = listFactory(listEl); + this.list_.wrapFocus = true; + } + this.focusTrapFactory_ = focusTrapFactory; + } + + initialSyncWithDOM() { + const {MODAL} = cssClasses; + const {SCRIM_SELECTOR} = strings; + + this.scrim_ = (this.root_.parentNode as Element).querySelector(SCRIM_SELECTOR); + + if (this.scrim_ && this.root_.classList.contains(MODAL)) { + this.handleScrimClick_ = () => (this.foundation_ as MDCModalDrawerFoundation).handleScrimClick(); + this.scrim_.addEventListener('click', this.handleScrimClick_); + this.focusTrap_ = util.createFocusTrapInstance(this.root_ as HTMLElement, this.focusTrapFactory_); + } + + this.handleKeydown_ = (evt) => this.foundation_.handleKeydown(evt); + this.handleTransitionEnd_ = (evt) => this.foundation_.handleTransitionEnd(evt); + + this.listen('keydown', this.handleKeydown_); + this.listen('transitionend', this.handleTransitionEnd_); + } + + destroy() { + this.unlisten('keydown', this.handleKeydown_); + this.unlisten('transitionend', this.handleTransitionEnd_); + + if (this.list_) { + this.list_.destroy(); + } + + const {MODAL} = cssClasses; + if (this.scrim_ && this.handleScrimClick_ && this.root_.classList.contains(MODAL)) { + this.scrim_.removeEventListener('click', this.handleScrimClick_); + // Ensure drawer is closed to hide scrim and release focus + this.open = false; + } + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + // tslint:disable:object-literal-sort-keys + const adapter: MDCDrawerAdapter = { + addClass: (className) => this.root_.classList.add(className), + removeClass: (className) => this.root_.classList.remove(className), + hasClass: (className) => this.root_.classList.contains(className), + elementHasClass: (element, className) => element.classList.contains(className), + saveFocus: () => this.previousFocus_ = document.activeElement, + restoreFocus: () => { + const previousFocus = this.previousFocus_ as HTMLOrSVGElement | null; + if (previousFocus && previousFocus.focus && this.root_.contains(document.activeElement)) { + previousFocus.focus(); + } + }, + focusActiveNavigationItem: () => { + const activeNavItemEl = + this.root_.querySelector(`.${MDCListFoundation.cssClasses.LIST_ITEM_ACTIVATED_CLASS}`); + if (activeNavItemEl) { + activeNavItemEl.focus(); + } + }, + notifyClose: () => this.emit(strings.CLOSE_EVENT, {}, true /* shouldBubble */), + notifyOpen: () => this.emit(strings.OPEN_EVENT, {}, true /* shouldBubble */), + trapFocus: () => this.focusTrap_!.activate(), + releaseFocus: () => this.focusTrap_!.deactivate(), + }; + // tslint:enable:object-literal-sort-keys + + const {DISMISSIBLE, MODAL} = cssClasses; + if (this.root_.classList.contains(DISMISSIBLE)) { + return new MDCDismissibleDrawerFoundation(adapter); + } else if (this.root_.classList.contains(MODAL)) { + return new MDCModalDrawerFoundation(adapter); + } else { + throw new Error( + `MDCDrawer: Failed to instantiate component. Supported variants are ${DISMISSIBLE} and ${MODAL}.`); + } + } +} diff --git a/packages/mdc-drawer/dismissible/foundation.ts b/packages/mdc-drawer/dismissible/foundation.ts index 3432039e3ad..171bbaa885c 100644 --- a/packages/mdc-drawer/dismissible/foundation.ts +++ b/packages/mdc-drawer/dismissible/foundation.ts @@ -25,7 +25,7 @@ import {MDCFoundation} from '@material/base/foundation'; import {MDCDrawerAdapter} from '../adapter'; import {cssClasses, strings} from '../constants'; -class MDCDismissibleDrawerFoundation extends MDCFoundation { +export class MDCDismissibleDrawerFoundation extends MDCFoundation { static get strings() { return strings; } @@ -180,4 +180,4 @@ class MDCDismissibleDrawerFoundation extends MDCFoundation { } } -export {MDCDismissibleDrawerFoundation as default, MDCDismissibleDrawerFoundation}; +export default MDCDismissibleDrawerFoundation; diff --git a/packages/mdc-drawer/index.ts b/packages/mdc-drawer/index.ts index 5f57eaeb2f3..9d6d8c54078 100644 --- a/packages/mdc-drawer/index.ts +++ b/packages/mdc-drawer/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2016 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,140 +21,10 @@ * THE SOFTWARE. */ -import {MDCComponent} from '@material/base/component'; -import {SpecificEventListener} from '@material/base/index'; -import {MDCListFoundation} from '@material/list/foundation'; -import {MDCList} from '@material/list/index'; -import * as createFocusTrap from 'focus-trap'; -import {MDCDrawerAdapter} from './adapter'; -import {strings} from './constants'; -import {MDCDismissibleDrawerFoundation} from './dismissible/foundation'; -import {MDCModalDrawerFoundation} from './modal/foundation'; -import {FocusTrapFactory, ListFactory} from './types'; import * as util from './util'; -class MDCDrawer extends MDCComponent { - static attachTo(root: Element): MDCDrawer { - return new MDCDrawer(root); - } - - /** - * Returns true if drawer is in the open position. - */ - get open(): boolean { - return this.foundation_.isOpen(); - } - - /** - * Toggles the drawer open and closed. - */ - set open(isOpen: boolean) { - if (isOpen) { - this.foundation_.open(); - } else { - this.foundation_.close(); - } - } - - private previousFocus_?: Element | null; - private scrim_!: HTMLElement | null; // assigned in initialSyncWithDOM() - private list_?: MDCList; // assigned in initialize() - - private focusTrap_?: createFocusTrap.FocusTrap; // assigned in initialSyncWithDOM() - private focusTrapFactory_!: FocusTrapFactory; // assigned in initialize() - - private handleScrimClick_?: SpecificEventListener<'click'>; // initialized in initialSyncWithDOM() - private handleKeydown_!: SpecificEventListener<'keydown'>; // initialized in initialSyncWithDOM() - private handleTransitionEnd_!: SpecificEventListener<'transitionend'>; // initialized in initialSyncWithDOM() - - initialize( - focusTrapFactory: FocusTrapFactory = createFocusTrap as unknown as FocusTrapFactory, - listFactory: ListFactory = (el) => new MDCList(el)) { - const listEl = this.root_.querySelector(`.${MDCListFoundation.cssClasses.ROOT}`); - if (listEl) { - this.list_ = listFactory(listEl); - this.list_.wrapFocus = true; - } - this.focusTrapFactory_ = focusTrapFactory; - } - - initialSyncWithDOM() { - const {MODAL} = MDCDismissibleDrawerFoundation.cssClasses; - const {SCRIM_SELECTOR} = MDCDismissibleDrawerFoundation.strings; - - this.scrim_ = (this.root_.parentNode as Element).querySelector(SCRIM_SELECTOR); - - if (this.scrim_ && this.root_.classList.contains(MODAL)) { - this.handleScrimClick_ = () => (this.foundation_ as MDCModalDrawerFoundation).handleScrimClick(); - this.scrim_.addEventListener('click', this.handleScrimClick_); - this.focusTrap_ = util.createFocusTrapInstance(this.root_ as HTMLElement, this.focusTrapFactory_); - } - - this.handleKeydown_ = (evt) => this.foundation_.handleKeydown(evt); - this.handleTransitionEnd_ = (evt) => this.foundation_.handleTransitionEnd(evt); - - this.listen('keydown', this.handleKeydown_); - this.listen('transitionend', this.handleTransitionEnd_); - } - - destroy() { - this.unlisten('keydown', this.handleKeydown_); - this.unlisten('transitionend', this.handleTransitionEnd_); - - if (this.list_) { - this.list_.destroy(); - } - - const {MODAL} = MDCDismissibleDrawerFoundation.cssClasses; - if (this.scrim_ && this.handleScrimClick_ && this.root_.classList.contains(MODAL)) { - this.scrim_.removeEventListener('click', this.handleScrimClick_); - // Ensure drawer is closed to hide scrim and release focus - this.open = false; - } - } - - getDefaultFoundation(): MDCDismissibleDrawerFoundation { - // tslint:disable:object-literal-sort-keys - const adapter: MDCDrawerAdapter = { - addClass: (className) => this.root_.classList.add(className), - removeClass: (className) => this.root_.classList.remove(className), - hasClass: (className) => this.root_.classList.contains(className), - elementHasClass: (element, className) => element.classList.contains(className), - saveFocus: () => this.previousFocus_ = document.activeElement, - restoreFocus: () => { - const previousFocus = this.previousFocus_ as HTMLOrSVGElement | null; - if (previousFocus && previousFocus.focus && this.root_.contains(document.activeElement)) { - previousFocus.focus(); - } - }, - focusActiveNavigationItem: () => { - const activeNavItemEl = - this.root_.querySelector(`.${MDCListFoundation.cssClasses.LIST_ITEM_ACTIVATED_CLASS}`); - if (activeNavItemEl) { - activeNavItemEl.focus(); - } - }, - notifyClose: () => this.emit(strings.CLOSE_EVENT, {}, true /* shouldBubble */), - notifyOpen: () => this.emit(strings.OPEN_EVENT, {}, true /* shouldBubble */), - trapFocus: () => this.focusTrap_!.activate(), - releaseFocus: () => this.focusTrap_!.deactivate(), - }; - // tslint:enable:object-literal-sort-keys - - const {DISMISSIBLE, MODAL} = MDCDismissibleDrawerFoundation.cssClasses; - if (this.root_.classList.contains(DISMISSIBLE)) { - return new MDCDismissibleDrawerFoundation(adapter); - } else if (this.root_.classList.contains(MODAL)) { - return new MDCModalDrawerFoundation(adapter); - } else { - throw new Error( - `MDCDrawer: Failed to instantiate component. Supported variants are ${DISMISSIBLE} and ${MODAL}.`); - } - } -} - -export {MDCDrawer as default, MDCDrawer, util}; +export {util}; +export * from './adapter'; +export * from './component'; export * from './dismissible/foundation'; export * from './modal/foundation'; -export * from './adapter'; -export * from './types'; diff --git a/packages/mdc-drawer/modal/foundation.ts b/packages/mdc-drawer/modal/foundation.ts index 3f683d550ff..f0e83398842 100644 --- a/packages/mdc-drawer/modal/foundation.ts +++ b/packages/mdc-drawer/modal/foundation.ts @@ -24,7 +24,7 @@ import {MDCDismissibleDrawerFoundation} from '../dismissible/foundation'; /* istanbul ignore next: subclass is not a branch statement */ -class MDCModalDrawerFoundation extends MDCDismissibleDrawerFoundation { +export class MDCModalDrawerFoundation extends MDCDismissibleDrawerFoundation { /** * Called when drawer finishes open animation. */ @@ -47,4 +47,4 @@ class MDCModalDrawerFoundation extends MDCDismissibleDrawerFoundation { } } -export {MDCModalDrawerFoundation as default, MDCModalDrawerFoundation}; +export default MDCModalDrawerFoundation; diff --git a/packages/mdc-drawer/util.ts b/packages/mdc-drawer/util.ts index 0b66f546d07..b84630c8e2e 100644 --- a/packages/mdc-drawer/util.ts +++ b/packages/mdc-drawer/util.ts @@ -22,11 +22,15 @@ */ import * as createFocusTrap from 'focus-trap'; -import {FocusTrapFactory} from './types'; + +export type MDCDrawerFocusTrapFactory = ( + element: HTMLElement | string, + userOptions?: createFocusTrap.Options, +) => createFocusTrap.FocusTrap; export function createFocusTrapInstance( surfaceEl: HTMLElement, - focusTrapFactory: FocusTrapFactory = createFocusTrap as unknown as FocusTrapFactory, + focusTrapFactory: MDCDrawerFocusTrapFactory = createFocusTrap as unknown as MDCDrawerFocusTrapFactory, ): createFocusTrap.FocusTrap { return focusTrapFactory(surfaceEl, { clickOutsideDeactivates: true, // Allow handling of scrim clicks. diff --git a/packages/mdc-feature-targeting/_functions.scss b/packages/mdc-feature-targeting/_functions.scss index ea844eff2b3..1aaa326ecfb 100644 --- a/packages/mdc-feature-targeting/_functions.scss +++ b/packages/mdc-feature-targeting/_functions.scss @@ -91,7 +91,8 @@ @return ( op: without, - queries: ($feature-query) + // NOTE: we need to use `append`, just putting parens around a single value doesn't make it a list in Sass. + queries: append((), $feature-query) ); } diff --git a/packages/mdc-floating-label/adapter.ts b/packages/mdc-floating-label/adapter.ts index 06427d70a8a..bd0ef1db324 100644 --- a/packages/mdc-floating-label/adapter.ts +++ b/packages/mdc-floating-label/adapter.ts @@ -21,7 +21,7 @@ * THE SOFTWARE. */ -import {EventType, SpecificEventListener} from '@material/base/index'; +import {EventType, SpecificEventListener} from '@material/base/types'; /** * Defines the shape of the adapter expected by the foundation. @@ -30,7 +30,7 @@ import {EventType, SpecificEventListener} from '@material/base/index'; * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCFloatingLabelAdapter { +export interface MDCFloatingLabelAdapter { /** * Adds a class to the label element. */ @@ -56,5 +56,3 @@ interface MDCFloatingLabelAdapter { */ deregisterInteractionHandler(evtType: K, handler: SpecificEventListener): void; } - -export {MDCFloatingLabelAdapter as default, MDCFloatingLabelAdapter}; diff --git a/packages/mdc-floating-label/component.ts b/packages/mdc-floating-label/component.ts new file mode 100644 index 00000000000..e6d58bb4d94 --- /dev/null +++ b/packages/mdc-floating-label/component.ts @@ -0,0 +1,69 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {MDCFloatingLabelAdapter} from './adapter'; +import {MDCFloatingLabelFoundation} from './foundation'; + +export type MDCFloatingLabelFactory = (el: Element, foundation?: MDCFloatingLabelFoundation) => MDCFloatingLabel; + +export class MDCFloatingLabel extends MDCComponent { + static attachTo(root: Element): MDCFloatingLabel { + return new MDCFloatingLabel(root); + } + + /** + * Styles the label to produce the label shake for errors. + * @param shouldShake If true, shakes the label by adding a CSS class; otherwise, stops shaking by removing the class. + */ + shake(shouldShake: boolean) { + this.foundation_.shake(shouldShake); + } + + /** + * Styles the label to float/dock. + * @param shouldFloat If true, floats the label by adding a CSS class; otherwise, docks it by removing the class. + */ + float(shouldFloat: boolean) { + this.foundation_.float(shouldFloat); + } + + getWidth(): number { + return this.foundation_.getWidth(); + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + // tslint:disable:object-literal-sort-keys + const adapter: MDCFloatingLabelAdapter = { + addClass: (className) => this.root_.classList.add(className), + removeClass: (className) => this.root_.classList.remove(className), + getWidth: () => this.root_.scrollWidth, + registerInteractionHandler: (evtType, handler) => this.listen(evtType, handler), + deregisterInteractionHandler: (evtType, handler) => this.unlisten(evtType, handler), + }; + // tslint:enable:object-literal-sort-keys + return new MDCFloatingLabelFoundation(adapter); + } +} diff --git a/packages/mdc-floating-label/constants.ts b/packages/mdc-floating-label/constants.ts index 96dcf2b93fa..71c59a3b5a7 100644 --- a/packages/mdc-floating-label/constants.ts +++ b/packages/mdc-floating-label/constants.ts @@ -21,10 +21,8 @@ * THE SOFTWARE. */ -const cssClasses = { +export const cssClasses = { LABEL_FLOAT_ABOVE: 'mdc-floating-label--float-above', LABEL_SHAKE: 'mdc-floating-label--shake', ROOT: 'mdc-floating-label', }; - -export {cssClasses}; diff --git a/packages/mdc-floating-label/foundation.ts b/packages/mdc-floating-label/foundation.ts index e6782d8d853..12acb3e29db 100644 --- a/packages/mdc-floating-label/foundation.ts +++ b/packages/mdc-floating-label/foundation.ts @@ -22,11 +22,11 @@ */ import {MDCFoundation} from '@material/base/foundation'; -import {SpecificEventListener} from '@material/base/index'; +import {SpecificEventListener} from '@material/base/types'; import {MDCFloatingLabelAdapter} from './adapter'; import {cssClasses} from './constants'; -class MDCFloatingLabelFoundation extends MDCFoundation { +export class MDCFloatingLabelFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } @@ -102,4 +102,4 @@ class MDCFloatingLabelFoundation extends MDCFoundation } } -export {MDCFloatingLabelFoundation as default, MDCFloatingLabelFoundation}; +export default MDCFloatingLabelFoundation; diff --git a/packages/mdc-floating-label/index.ts b/packages/mdc-floating-label/index.ts index 20b510e2d03..f8c89ac94f3 100644 --- a/packages/mdc-floating-label/index.ts +++ b/packages/mdc-floating-label/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2016 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,47 +21,6 @@ * THE SOFTWARE. */ -import {MDCComponent} from '@material/base/component'; -import {MDCFloatingLabelFoundation} from './foundation'; - -class MDCFloatingLabel extends MDCComponent { - static attachTo(root: Element): MDCFloatingLabel { - return new MDCFloatingLabel(root); - } - - /** - * Styles the label to produce the label shake for errors. - * @param shouldShake If true, shakes the label by adding a CSS class; otherwise, stops shaking by removing the class. - */ - shake(shouldShake: boolean) { - this.foundation_.shake(shouldShake); - } - - /** - * Styles the label to float/dock. - * @param shouldFloat If true, floats the label by adding a CSS class; otherwise, docks it by removing the class. - */ - float(shouldFloat: boolean) { - this.foundation_.float(shouldFloat); - } - - getWidth(): number { - return this.foundation_.getWidth(); - } - - getDefaultFoundation(): MDCFloatingLabelFoundation { - // tslint:disable:object-literal-sort-keys - return new MDCFloatingLabelFoundation({ - addClass: (className) => this.root_.classList.add(className), - removeClass: (className) => this.root_.classList.remove(className), - getWidth: () => this.root_.scrollWidth, - registerInteractionHandler: (evtType, handler) => this.root_.addEventListener(evtType, handler), - deregisterInteractionHandler: (evtType, handler) => this.root_.removeEventListener(evtType, handler), - }); - // tslint:enable:object-literal-sort-keys - } -} - -export {MDCFloatingLabel as default, MDCFloatingLabel}; export * from './adapter'; +export * from './component'; export * from './foundation'; diff --git a/packages/mdc-form-field/adapter.ts b/packages/mdc-form-field/adapter.ts index fa47c6908fd..51f4aedb047 100644 --- a/packages/mdc-form-field/adapter.ts +++ b/packages/mdc-form-field/adapter.ts @@ -21,7 +21,7 @@ * THE SOFTWARE. */ -import {EventType, SpecificEventListener} from '@material/base/index'; +import {EventType, SpecificEventListener} from '@material/base/types'; /** * Defines the shape of the adapter expected by the foundation. @@ -30,11 +30,9 @@ import {EventType, SpecificEventListener} from '@material/base/index'; * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCFormFieldAdapter { +export interface MDCFormFieldAdapter { activateInputRipple(): void; deactivateInputRipple(): void; deregisterInteractionHandler(evtType: K, handler: SpecificEventListener): void; registerInteractionHandler(evtType: K, handler: SpecificEventListener): void; } - -export {MDCFormFieldAdapter as default, MDCFormFieldAdapter}; diff --git a/packages/mdc-form-field/component.ts b/packages/mdc-form-field/component.ts new file mode 100644 index 00000000000..caef1b4f3fc --- /dev/null +++ b/packages/mdc-form-field/component.ts @@ -0,0 +1,76 @@ +/** + * @license + * Copyright 2017 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {MDCSelectionControl} from '@material/selection-control/types'; +import {MDCFormFieldAdapter} from './adapter'; +import {MDCFormFieldFoundation} from './foundation'; + +export class MDCFormField extends MDCComponent { + static attachTo(root: HTMLElement) { + return new MDCFormField(root); + } + + private input_?: MDCSelectionControl; + + set input(input: MDCSelectionControl | undefined) { + this.input_ = input; + } + + get input(): MDCSelectionControl | undefined { + return this.input_; + } + + private get label_(): Element | null { + const {LABEL_SELECTOR} = MDCFormFieldFoundation.strings; + return this.root_.querySelector(LABEL_SELECTOR); + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + const adapter: MDCFormFieldAdapter = { + activateInputRipple: () => { + if (this.input_ && this.input_.ripple) { + this.input_.ripple.activate(); + } + }, + deactivateInputRipple: () => { + if (this.input_ && this.input_.ripple) { + this.input_.ripple.deactivate(); + } + }, + deregisterInteractionHandler: (evtType, handler) => { + if (this.label_) { + this.label_.removeEventListener(evtType, handler); + } + }, + registerInteractionHandler: (evtType, handler) => { + if (this.label_) { + this.label_.addEventListener(evtType, handler); + } + }, + }; + return new MDCFormFieldFoundation(adapter); + } +} diff --git a/packages/mdc-form-field/constants.ts b/packages/mdc-form-field/constants.ts index 5bc8f91863b..2a1c4d2aa81 100644 --- a/packages/mdc-form-field/constants.ts +++ b/packages/mdc-form-field/constants.ts @@ -21,12 +21,10 @@ * THE SOFTWARE. */ -const cssClasses = { +export const cssClasses = { ROOT: 'mdc-form-field', }; -const strings = { +export const strings = { LABEL_SELECTOR: '.mdc-form-field > label', }; - -export {cssClasses, strings}; diff --git a/packages/mdc-form-field/foundation.ts b/packages/mdc-form-field/foundation.ts index 5ebe2553c29..4d7791f9874 100644 --- a/packages/mdc-form-field/foundation.ts +++ b/packages/mdc-form-field/foundation.ts @@ -25,7 +25,7 @@ import {MDCFoundation} from '@material/base/foundation'; import {MDCFormFieldAdapter} from './adapter'; import {cssClasses, strings} from './constants'; -class MDCFormFieldFoundation extends MDCFoundation { +export class MDCFormFieldFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } @@ -65,4 +65,4 @@ class MDCFormFieldFoundation extends MDCFoundation { } } -export {MDCFormFieldFoundation as default, MDCFormFieldFoundation}; +export default MDCFormFieldFoundation; diff --git a/packages/mdc-form-field/index.ts b/packages/mdc-form-field/index.ts index b64e2366ace..f8c89ac94f3 100644 --- a/packages/mdc-form-field/index.ts +++ b/packages/mdc-form-field/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,56 +21,6 @@ * THE SOFTWARE. */ -import {MDCComponent} from '@material/base/component'; -import {MDCSelectionControl} from '@material/selection-control/index'; -import {MDCFormFieldFoundation} from './foundation'; - -class MDCFormField extends MDCComponent { - static attachTo(root: HTMLElement) { - return new MDCFormField(root); - } - - private input_?: MDCSelectionControl; - - set input(input: MDCSelectionControl | undefined) { - this.input_ = input; - } - - get input(): MDCSelectionControl | undefined { - return this.input_; - } - - private get label_(): Element | null { - const {LABEL_SELECTOR} = MDCFormFieldFoundation.strings; - return this.root_.querySelector(LABEL_SELECTOR); - } - - getDefaultFoundation() { - return new MDCFormFieldFoundation({ - activateInputRipple: () => { - if (this.input_ && this.input_.ripple) { - this.input_.ripple.activate(); - } - }, - deactivateInputRipple: () => { - if (this.input_ && this.input_.ripple) { - this.input_.ripple.deactivate(); - } - }, - deregisterInteractionHandler: (evtType, handler) => { - if (this.label_) { - this.label_.removeEventListener(evtType, handler); - } - }, - registerInteractionHandler: (evtType, handler) => { - if (this.label_) { - this.label_.addEventListener(evtType, handler); - } - }, - }); - } -} - -export {MDCFormField as default, MDCFormField}; export * from './adapter'; +export * from './component'; export * from './foundation'; diff --git a/packages/mdc-grid-list/adapter.ts b/packages/mdc-grid-list/adapter.ts index f258b10d053..54ab69dd3de 100644 --- a/packages/mdc-grid-list/adapter.ts +++ b/packages/mdc-grid-list/adapter.ts @@ -21,15 +21,20 @@ * THE SOFTWARE. */ -interface MDCGridListAdapter { +/** + * Defines the shape of the adapter expected by the foundation. + * Implement this adapter for your framework of choice to delegate updates to + * the component in your framework of choice. See architecture documentation + * for more details. + * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md + */ +export interface MDCGridListAdapter { deregisterResizeHandler(handler: EventListener): void; getNumberOfTiles(): number; getOffsetWidth(): number; getOffsetWidthForTileAtIndex(index: number): number; registerResizeHandler(handler: EventListener): void; setStyleForTilesElement( - property: Exclude, value: string | null, + property: Exclude, value: string | null, ): void; } - -export {MDCGridListAdapter as default, MDCGridListAdapter}; diff --git a/packages/mdc-grid-list/component.ts b/packages/mdc-grid-list/component.ts new file mode 100644 index 00000000000..ccee1065ee3 --- /dev/null +++ b/packages/mdc-grid-list/component.ts @@ -0,0 +1,54 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {MDCGridListAdapter} from './adapter'; +import {MDCGridListFoundation} from './foundation'; + +export class MDCGridList extends MDCComponent { + static attachTo(root: Element) { + return new MDCGridList(root); + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + const adapter: MDCGridListAdapter = { + deregisterResizeHandler: (handler) => window.removeEventListener('resize', handler), + getNumberOfTiles: () => { + return this.root_.querySelectorAll(MDCGridListFoundation.strings.TILE_SELECTOR).length; + }, + getOffsetWidth: () => (this.root_ as HTMLElement).offsetWidth, + getOffsetWidthForTileAtIndex: (index) => { + const tileEl = this.root_.querySelectorAll(MDCGridListFoundation.strings.TILE_SELECTOR)[index]; + return tileEl.offsetWidth; + }, + registerResizeHandler: (handler) => window.addEventListener('resize', handler), + setStyleForTilesElement: (property, value) => { + const tilesEl = this.root_.querySelector(MDCGridListFoundation.strings.TILES_SELECTOR); + tilesEl!.style[property] = value; + }, + }; + return new MDCGridListFoundation(adapter); + } +} diff --git a/packages/mdc-grid-list/foundation.ts b/packages/mdc-grid-list/foundation.ts index c76cf37b1e6..5929ca29354 100644 --- a/packages/mdc-grid-list/foundation.ts +++ b/packages/mdc-grid-list/foundation.ts @@ -25,7 +25,7 @@ import {MDCFoundation} from '@material/base/foundation'; import {MDCGridListAdapter} from './adapter'; import {strings} from './constants'; -class MDCGridListFoundation extends MDCFoundation { +export class MDCGridListFoundation extends MDCFoundation { static get strings() { return strings; } @@ -81,4 +81,4 @@ class MDCGridListFoundation extends MDCFoundation { } } -export {MDCGridListFoundation as default, MDCGridListFoundation}; +export default MDCGridListFoundation; diff --git a/packages/mdc-grid-list/index.ts b/packages/mdc-grid-list/index.ts index 36e550ff3ec..f8c89ac94f3 100644 --- a/packages/mdc-grid-list/index.ts +++ b/packages/mdc-grid-list/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2016 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,34 +21,6 @@ * THE SOFTWARE. */ -import {MDCComponent} from '@material/base/component'; -import {MDCGridListFoundation} from './foundation'; - -class MDCGridList extends MDCComponent { - static attachTo(root: Element) { - return new MDCGridList(root); - } - - getDefaultFoundation() { - return new MDCGridListFoundation({ - deregisterResizeHandler: (handler) => window.removeEventListener('resize', handler), - getNumberOfTiles: () => { - return this.root_.querySelectorAll(MDCGridListFoundation.strings.TILE_SELECTOR).length; - }, - getOffsetWidth: () => (this.root_ as HTMLElement).offsetWidth, - getOffsetWidthForTileAtIndex: (index) => { - const tileEl = this.root_.querySelectorAll(MDCGridListFoundation.strings.TILE_SELECTOR)[index]; - return tileEl.offsetWidth; - }, - registerResizeHandler: (handler) => window.addEventListener('resize', handler), - setStyleForTilesElement: (property, value) => { - const tilesEl = this.root_.querySelector(MDCGridListFoundation.strings.TILES_SELECTOR); - tilesEl!.style[property] = value; - }, - }); - } -} - -export {MDCGridList as default, MDCGridList}; export * from './adapter'; +export * from './component'; export * from './foundation'; diff --git a/packages/mdc-icon-button/adapter.ts b/packages/mdc-icon-button/adapter.ts index b381f4dee45..bfddd0903d6 100644 --- a/packages/mdc-icon-button/adapter.ts +++ b/packages/mdc-icon-button/adapter.ts @@ -21,7 +21,7 @@ * THE SOFTWARE. */ -import {IconButtonToggleEvent} from './types'; +import {MDCIconButtonToggleEventDetail} from './types'; /** * Defines the shape of the adapter expected by the foundation. @@ -30,7 +30,7 @@ import {IconButtonToggleEvent} from './types'; * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCIconButtonToggleAdapter { +export interface MDCIconButtonToggleAdapter { addClass(className: string): void; removeClass(className: string): void; @@ -39,7 +39,5 @@ interface MDCIconButtonToggleAdapter { setAttr(attrName: string, attrValue: string): void; - notifyChange(evtData: IconButtonToggleEvent): void; + notifyChange(evtData: MDCIconButtonToggleEventDetail): void; } - -export {MDCIconButtonToggleAdapter as default, MDCIconButtonToggleAdapter}; diff --git a/packages/mdc-icon-button/component.ts b/packages/mdc-icon-button/component.ts new file mode 100644 index 00000000000..52e1dc592c7 --- /dev/null +++ b/packages/mdc-icon-button/component.ts @@ -0,0 +1,84 @@ +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {SpecificEventListener} from '@material/base/types'; +import {MDCRipple} from '@material/ripple/component'; +import {MDCIconButtonToggleAdapter} from './adapter'; +import {MDCIconButtonToggleFoundation} from './foundation'; +import {MDCIconButtonToggleEventDetail} from './types'; + +const {strings} = MDCIconButtonToggleFoundation; + +export class MDCIconButtonToggle extends MDCComponent { + static attachTo(root: HTMLElement) { + return new MDCIconButtonToggle(root); + } + + protected root_!: HTMLElement; // assigned in MDCComponent constructor + + private readonly ripple_: MDCRipple = this.createRipple_(); + private handleClick_!: SpecificEventListener<'click'>; // assigned in initialSyncWithDOM() + + initialSyncWithDOM() { + this.handleClick_ = () => this.foundation_.handleClick(); + this.listen('click', this.handleClick_); + } + + destroy() { + this.unlisten('click', this.handleClick_); + this.ripple_.destroy(); + super.destroy(); + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + const adapter: MDCIconButtonToggleAdapter = { + addClass: (className) => this.root_.classList.add(className), + hasClass: (className) => this.root_.classList.contains(className), + notifyChange: (evtData) => this.emit(strings.CHANGE_EVENT, evtData), + removeClass: (className) => this.root_.classList.remove(className), + setAttr: (attrName, attrValue) => this.root_.setAttribute(attrName, attrValue), + }; + return new MDCIconButtonToggleFoundation(adapter); + } + + get ripple(): MDCRipple { + return this.ripple_; + } + + get on(): boolean { + return this.foundation_.isOn(); + } + + set on(isOn: boolean) { + this.foundation_.toggle(isOn); + } + + private createRipple_(): MDCRipple { + const ripple = new MDCRipple(this.root_); + ripple.unbounded = true; + return ripple; + } +} diff --git a/packages/mdc-icon-button/constants.ts b/packages/mdc-icon-button/constants.ts index 69db5439980..83b162d87ca 100644 --- a/packages/mdc-icon-button/constants.ts +++ b/packages/mdc-icon-button/constants.ts @@ -21,14 +21,12 @@ * THE SOFTWARE. */ -const cssClasses = { +export const cssClasses = { ICON_BUTTON_ON: 'mdc-icon-button--on', ROOT: 'mdc-icon-button', }; -const strings = { +export const strings = { ARIA_PRESSED: 'aria-pressed', CHANGE_EVENT: 'MDCIconButtonToggle:change', }; - -export {cssClasses, strings}; diff --git a/packages/mdc-icon-button/foundation.ts b/packages/mdc-icon-button/foundation.ts index 10d2de9aac3..fbc0e4c0731 100644 --- a/packages/mdc-icon-button/foundation.ts +++ b/packages/mdc-icon-button/foundation.ts @@ -25,7 +25,7 @@ import {MDCFoundation} from '@material/base/foundation'; import {MDCIconButtonToggleAdapter} from './adapter'; import {cssClasses, strings} from './constants'; -class MDCIconButtonToggleFoundation extends MDCFoundation { +export class MDCIconButtonToggleFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } @@ -72,4 +72,4 @@ class MDCIconButtonToggleFoundation extends MDCFoundation { - static attachTo(root: HTMLElement) { - return new MDCIconButtonToggle(root); - } - - protected root_!: HTMLElement; // assigned in MDCComponent constructor - - private ripple_: MDCRipple = this.initRipple_(); - private handleClick_!: SpecificEventListener<'click'>; // assigned in initialSyncWithDOM() - - initialSyncWithDOM() { - this.handleClick_ = () => this.foundation_.handleClick(); - this.root_.addEventListener('click', this.handleClick_); - } - - destroy() { - this.root_.removeEventListener('click', this.handleClick_); - this.ripple_.destroy(); - super.destroy(); - } - - getDefaultFoundation(): MDCIconButtonToggleFoundation { - return new MDCIconButtonToggleFoundation({ - addClass: (className) => this.root_.classList.add(className), - hasClass: (className) => this.root_.classList.contains(className), - notifyChange: (evtData) => this.emit(MDCIconButtonToggleFoundation.strings.CHANGE_EVENT, evtData), - removeClass: (className) => this.root_.classList.remove(className), - setAttr: (attrName, attrValue) => this.root_.setAttribute(attrName, attrValue), - }); - } - - get ripple(): MDCRipple { - return this.ripple_; - } - - get on(): boolean { - return this.foundation_.isOn(); - } - - set on(isOn: boolean) { - this.foundation_.toggle(isOn); - } - - private initRipple_(): MDCRipple { - const ripple = new MDCRipple(this.root_); - ripple.unbounded = true; - return ripple; - } -} - -export {MDCIconButtonToggle as default, MDCIconButtonToggle}; export * from './adapter'; +export * from './component'; export * from './foundation'; export * from './types'; diff --git a/packages/mdc-icon-button/types.ts b/packages/mdc-icon-button/types.ts index 7b41778acdf..72718b8af87 100644 --- a/packages/mdc-icon-button/types.ts +++ b/packages/mdc-icon-button/types.ts @@ -21,6 +21,6 @@ * THE SOFTWARE. */ -export interface IconButtonToggleEvent { +export interface MDCIconButtonToggleEventDetail { isOn: boolean; } diff --git a/packages/mdc-line-ripple/adapter.ts b/packages/mdc-line-ripple/adapter.ts index fad35980e40..80677c6bda7 100644 --- a/packages/mdc-line-ripple/adapter.ts +++ b/packages/mdc-line-ripple/adapter.ts @@ -21,7 +21,7 @@ * THE SOFTWARE. */ -import {EventType, SpecificEventListener} from '@material/base/index'; +import {EventType, SpecificEventListener} from '@material/base/types'; /** * Defines the shape of the adapter expected by the foundation. @@ -30,7 +30,7 @@ import {EventType, SpecificEventListener} from '@material/base/index'; * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCLineRippleAdapter { +export interface MDCLineRippleAdapter { /** * Adds a class to the line ripple element. */ @@ -58,5 +58,3 @@ interface MDCLineRippleAdapter { */ deregisterEventHandler(evtType: K, handler: SpecificEventListener): void; } - -export {MDCLineRippleAdapter as default, MDCLineRippleAdapter}; diff --git a/packages/mdc-line-ripple/component.ts b/packages/mdc-line-ripple/component.ts new file mode 100644 index 00000000000..09208291d58 --- /dev/null +++ b/packages/mdc-line-ripple/component.ts @@ -0,0 +1,72 @@ +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {MDCLineRippleAdapter} from './adapter'; +import {MDCLineRippleFoundation} from './foundation'; + +export type MDCLineRippleFactory = (el: Element, foundation?: MDCLineRippleFoundation) => MDCLineRipple; + +export class MDCLineRipple extends MDCComponent { + static attachTo(root: Element): MDCLineRipple { + return new MDCLineRipple(root); + } + + /** + * Activates the line ripple + */ + activate() { + this.foundation_.activate(); + } + + /** + * Deactivates the line ripple + */ + deactivate() { + this.foundation_.deactivate(); + } + + /** + * Sets the transform origin given a user's click location. + * The `rippleCenter` is the x-coordinate of the middle of the ripple. + */ + setRippleCenter(xCoordinate: number) { + this.foundation_.setRippleCenter(xCoordinate); + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + // tslint:disable:object-literal-sort-keys + const adapter: MDCLineRippleAdapter = { + addClass: (className) => this.root_.classList.add(className), + removeClass: (className) => this.root_.classList.remove(className), + hasClass: (className) => this.root_.classList.contains(className), + setStyle: (propertyName, value) => (this.root_ as HTMLElement).style.setProperty(propertyName, value), + registerEventHandler: (evtType, handler) => this.listen(evtType, handler), + deregisterEventHandler: (evtType, handler) => this.unlisten(evtType, handler), + }; + // tslint:enable:object-literal-sort-keys + return new MDCLineRippleFoundation(adapter); + } +} diff --git a/packages/mdc-line-ripple/foundation.ts b/packages/mdc-line-ripple/foundation.ts index c9430d4a268..aebee468ec3 100644 --- a/packages/mdc-line-ripple/foundation.ts +++ b/packages/mdc-line-ripple/foundation.ts @@ -22,11 +22,11 @@ */ import {MDCFoundation} from '@material/base/foundation'; -import {SpecificEventListener} from '@material/base/index'; +import {SpecificEventListener} from '@material/base/types'; import {MDCLineRippleAdapter} from './adapter'; import {cssClasses} from './constants'; -class MDCLineRippleFoundation extends MDCFoundation { +export class MDCLineRippleFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } @@ -90,4 +90,4 @@ class MDCLineRippleFoundation extends MDCFoundation { } } -export {MDCLineRippleFoundation as default, MDCLineRippleFoundation}; +export default MDCLineRippleFoundation; diff --git a/packages/mdc-line-ripple/index.ts b/packages/mdc-line-ripple/index.ts index a7b21982fa2..f8c89ac94f3 100644 --- a/packages/mdc-line-ripple/index.ts +++ b/packages/mdc-line-ripple/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2018 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,50 +21,6 @@ * THE SOFTWARE. */ -import {MDCComponent} from '@material/base/component'; -import {MDCLineRippleFoundation} from './foundation'; - -class MDCLineRipple extends MDCComponent { - static attachTo(root: Element): MDCLineRipple { - return new MDCLineRipple(root); - } - - /** - * Activates the line ripple - */ - activate() { - this.foundation_.activate(); - } - - /** - * Deactivates the line ripple - */ - deactivate() { - this.foundation_.deactivate(); - } - - /** - * Sets the transform origin given a user's click location. - * The `rippleCenter` is the x-coordinate of the middle of the ripple. - */ - setRippleCenter(xCoordinate: number) { - this.foundation_.setRippleCenter(xCoordinate); - } - - getDefaultFoundation(): MDCLineRippleFoundation { - // tslint:disable:object-literal-sort-keys - return new MDCLineRippleFoundation({ - addClass: (className) => this.root_.classList.add(className), - removeClass: (className) => this.root_.classList.remove(className), - hasClass: (className) => this.root_.classList.contains(className), - setStyle: (propertyName, value) => (this.root_ as HTMLElement).style.setProperty(propertyName, value), - registerEventHandler: (evtType, handler) => this.root_.addEventListener(evtType, handler), - deregisterEventHandler: (evtType, handler) => this.root_.removeEventListener(evtType, handler), - }); - // tslint:enable:object-literal-sort-keys - } -} - -export {MDCLineRipple as default, MDCLineRipple}; export * from './adapter'; +export * from './component'; export * from './foundation'; diff --git a/packages/mdc-linear-progress/adapter.ts b/packages/mdc-linear-progress/adapter.ts index 977cda10c91..def5d5fa46b 100644 --- a/packages/mdc-linear-progress/adapter.ts +++ b/packages/mdc-linear-progress/adapter.ts @@ -28,7 +28,7 @@ * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCLinearProgressAdapter { +export interface MDCLinearProgressAdapter { addClass(className: string): void; getBuffer(): HTMLElement | null; getPrimaryBar(): HTMLElement | null; @@ -36,5 +36,3 @@ interface MDCLinearProgressAdapter { removeClass(className: string): void; setStyle(el: HTMLElement, styleProperty: string, value: string): void; } - -export {MDCLinearProgressAdapter as default, MDCLinearProgressAdapter}; diff --git a/packages/mdc-linear-progress/component.ts b/packages/mdc-linear-progress/component.ts new file mode 100644 index 00000000000..a25a5a4cd65 --- /dev/null +++ b/packages/mdc-linear-progress/component.ts @@ -0,0 +1,70 @@ +/** + * @license + * Copyright 2017 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {MDCLinearProgressAdapter} from './adapter'; +import {MDCLinearProgressFoundation} from './foundation'; + +export class MDCLinearProgress extends MDCComponent { + static attachTo(root: Element) { + return new MDCLinearProgress(root); + } + + set determinate(value: boolean) { + this.foundation_.setDeterminate(value); + } + + set progress(value: number) { + this.foundation_.setProgress(value); + } + + set buffer(value: number) { + this.foundation_.setBuffer(value); + } + + set reverse(value: boolean) { + this.foundation_.setReverse(value); + } + + open() { + this.foundation_.open(); + } + + close() { + this.foundation_.close(); + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + const adapter: MDCLinearProgressAdapter = { + addClass: (className: string) => this.root_.classList.add(className), + getBuffer: () => this.root_.querySelector(MDCLinearProgressFoundation.strings.BUFFER_SELECTOR), + getPrimaryBar: () => this.root_.querySelector(MDCLinearProgressFoundation.strings.PRIMARY_BAR_SELECTOR), + hasClass: (className: string) => this.root_.classList.contains(className), + removeClass: (className: string) => this.root_.classList.remove(className), + setStyle: (el: HTMLElement, styleProperty: string, value: string) => el.style.setProperty(styleProperty, value), + }; + return new MDCLinearProgressFoundation(adapter); + } +} diff --git a/packages/mdc-linear-progress/foundation.ts b/packages/mdc-linear-progress/foundation.ts index e7e875fc120..d5151942c80 100644 --- a/packages/mdc-linear-progress/foundation.ts +++ b/packages/mdc-linear-progress/foundation.ts @@ -21,12 +21,12 @@ * THE SOFTWARE. */ -import {getCorrectPropertyName} from '@material/animation/index'; +import {getCorrectPropertyName} from '@material/animation/util'; import {MDCFoundation} from '@material/base/foundation'; import {MDCLinearProgressAdapter} from './adapter'; import {cssClasses, strings} from './constants'; -class MDCLinearProgressFoundation extends MDCFoundation { +export class MDCLinearProgressFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } @@ -50,6 +50,10 @@ class MDCLinearProgressFoundation extends MDCFoundation) { + super({...MDCLinearProgressFoundation.defaultAdapter, ...adapter}); + } + init() { this.isDeterminate_ = !this.adapter_.hasClass(cssClasses.INDETERMINATE_CLASS); this.isReversed_ = this.adapter_.hasClass(cssClasses.REVERSED_CLASS); @@ -107,4 +111,4 @@ class MDCLinearProgressFoundation extends MDCFoundation { - static attachTo(root: Element) { - return new MDCLinearProgress(root); - } - - set determinate(value: boolean) { - this.foundation_.setDeterminate(value); - } - - set progress(value: number) { - this.foundation_.setProgress(value); - } - - set buffer(value: number) { - this.foundation_.setBuffer(value); - } - - set reverse(value: boolean) { - this.foundation_.setReverse(value); - } - - open() { - this.foundation_.open(); - } - - close() { - this.foundation_.close(); - } - - getDefaultFoundation() { - return new MDCLinearProgressFoundation({ - addClass: (className: string) => this.root_.classList.add(className), - getBuffer: () => this.root_.querySelector(MDCLinearProgressFoundation.strings.BUFFER_SELECTOR), - getPrimaryBar: () => this.root_.querySelector(MDCLinearProgressFoundation.strings.PRIMARY_BAR_SELECTOR), - hasClass: (className: string) => this.root_.classList.contains(className), - removeClass: (className: string) => this.root_.classList.remove(className), - setStyle: (el: HTMLElement, styleProperty: string, value: string) => el.style.setProperty(styleProperty, value), - }); - } -} - -export {MDCLinearProgress as default, MDCLinearProgress}; export * from './adapter'; +export * from './component'; export * from './foundation'; diff --git a/packages/mdc-list/README.md b/packages/mdc-list/README.md index f3e068f7fb6..fced9f445a3 100644 --- a/packages/mdc-list/README.md +++ b/packages/mdc-list/README.md @@ -542,8 +542,8 @@ Method Signature | Description `setWrapFocus(value: Boolean) => void` | Sets the list to allow the up arrow on the first element to focus the last element of the list and vice versa. `setVerticalOrientation(value: Boolean) => void` | Sets the list to an orientation causing the keys used for navigation to change. `true` results in the Up/Down arrow keys being used. `false` results in the Left/Right arrow keys being used. `setSingleSelection(value: Boolean) => void` | Sets the list to be a selection list. Enables the `enter` and `space` keys for selecting/deselecting a list item. -`getSelectedIndex() => ListIndex` | Gets the current selection state by returning selected index or list of indexes for checkbox based list. See [types.ts](./types.ts) for `ListIndex` type definition. -`setSelectedIndex(index: ListIndex) => void` | Sets the selection state to given index or list of indexes if it is checkbox based list. See [types.ts](./types.ts) for `ListIndex` type definition. +`getSelectedIndex() => MDCListIndex` | Gets the current selection state by returning selected index or list of indexes for checkbox based list. See [types.ts](./types.ts) for `MDCListIndex` type definition. +`setSelectedIndex(index: MDCListIndex) => void` | Sets the selection state to given index or list of indexes if it is checkbox based list. See [types.ts](./types.ts) for `MDCListIndex` type definition. `setUseActivated(useActivated: boolean) => void` | Sets the selection logic to apply/remove the `mdc-list-item--activated` class. `handleFocusIn(evt: Event) => void` | Handles the changing of `tabindex` to `0` for all button and anchor elements when a list item receives focus. `handleFocusOut(evt: Event) => void` | Handles the changing of `tabindex` to `-1` for all button and anchor elements when a list item loses focus. diff --git a/packages/mdc-list/adapter.ts b/packages/mdc-list/adapter.ts index ceb63a6fe0b..d6b78288489 100644 --- a/packages/mdc-list/adapter.ts +++ b/packages/mdc-list/adapter.ts @@ -28,7 +28,7 @@ * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCListAdapter { +export interface MDCListAdapter { getListItemCount(): number; getFocusedElementIndex(): number; @@ -82,5 +82,3 @@ interface MDCListAdapter { */ isFocusInsideList(): boolean; } - -export {MDCListAdapter as default, MDCListAdapter}; diff --git a/packages/mdc-list/component.ts b/packages/mdc-list/component.ts new file mode 100644 index 00000000000..3d6629f9cff --- /dev/null +++ b/packages/mdc-list/component.ts @@ -0,0 +1,262 @@ +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {SpecificEventListener} from '@material/base/types'; +import {ponyfill} from '@material/dom/index'; +import {MDCListAdapter} from './adapter'; +import {cssClasses, strings} from './constants'; +import {MDCListFoundation} from './foundation'; +import {MDCListActionEventDetail, MDCListIndex} from './types'; + +export type MDCListFactory = (el: Element, foundation?: MDCListFoundation) => MDCList; + +export class MDCList extends MDCComponent { + set vertical(value: boolean) { + this.foundation_.setVerticalOrientation(value); + } + + get listElements(): Element[] { + return [].slice.call(this.root_.querySelectorAll(strings.ENABLED_ITEMS_SELECTOR)); + } + + set wrapFocus(value: boolean) { + this.foundation_.setWrapFocus(value); + } + + set singleSelection(isSingleSelectionList: boolean) { + this.foundation_.setSingleSelection(isSingleSelectionList); + } + + get selectedIndex(): MDCListIndex { + return this.foundation_.getSelectedIndex(); + } + + set selectedIndex(index: MDCListIndex) { + this.foundation_.setSelectedIndex(index); + } + + static attachTo(root: Element) { + return new MDCList(root); + } + + private handleKeydown_!: SpecificEventListener<'keydown'>; // assigned in initialSyncWithDOM() + private handleClick_!: SpecificEventListener<'click'>; // assigned in initialSyncWithDOM() + private focusInEventListener_!: SpecificEventListener<'focus'>; // assigned in initialSyncWithDOM() + private focusOutEventListener_!: SpecificEventListener<'focus'>; // assigned in initialSyncWithDOM() + + initialSyncWithDOM() { + this.handleClick_ = this.handleClickEvent_.bind(this); + this.handleKeydown_ = this.handleKeydownEvent_.bind(this); + this.focusInEventListener_ = this.handleFocusInEvent_.bind(this); + this.focusOutEventListener_ = this.handleFocusOutEvent_.bind(this); + this.listen('keydown', this.handleKeydown_); + this.listen('click', this.handleClick_); + this.listen('focusin', this.focusInEventListener_); + this.listen('focusout', this.focusOutEventListener_); + this.layout(); + this.initializeListType(); + } + + destroy() { + this.unlisten('keydown', this.handleKeydown_); + this.unlisten('click', this.handleClick_); + this.unlisten('focusin', this.focusInEventListener_); + this.unlisten('focusout', this.focusOutEventListener_); + } + + layout() { + const direction = this.root_.getAttribute(strings.ARIA_ORIENTATION); + this.vertical = direction !== strings.ARIA_ORIENTATION_HORIZONTAL; + + // List items need to have at least tabindex=-1 to be focusable. + [].slice.call(this.root_.querySelectorAll('.mdc-list-item:not([tabindex])')) + .forEach((el: Element) => { + el.setAttribute('tabindex', '-1'); + }); + + // Child button/a elements are not tabbable until the list item is focused. + [].slice.call(this.root_.querySelectorAll(strings.FOCUSABLE_CHILD_ELEMENTS)) + .forEach((el: Element) => el.setAttribute('tabindex', '-1')); + + this.foundation_.layout(); + } + + /** + * Initialize selectedIndex value based on pre-selected checkbox list items, single selection or radio. + */ + initializeListType() { + const checkboxListItems = this.root_.querySelectorAll(strings.ARIA_ROLE_CHECKBOX_SELECTOR); + const singleSelectedListItem = this.root_.querySelector(` + .${cssClasses.LIST_ITEM_ACTIVATED_CLASS}, + .${cssClasses.LIST_ITEM_SELECTED_CLASS} + `); + const radioSelectedListItem = this.root_.querySelector(strings.ARIA_CHECKED_RADIO_SELECTOR); + + if (checkboxListItems.length) { + const preselectedItems = this.root_.querySelectorAll(strings.ARIA_CHECKED_CHECKBOX_SELECTOR); + this.selectedIndex = + [].map.call(preselectedItems, (listItem: Element) => this.listElements.indexOf(listItem)) as number[]; + } else if (singleSelectedListItem) { + if (singleSelectedListItem.classList.contains(cssClasses.LIST_ITEM_ACTIVATED_CLASS)) { + this.foundation_.setUseActivatedClass(true); + } + + this.singleSelection = true; + this.selectedIndex = this.listElements.indexOf(singleSelectedListItem); + } else if (radioSelectedListItem) { + this.selectedIndex = this.listElements.indexOf(radioSelectedListItem); + } + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + const adapter: MDCListAdapter = { + addClassForElementIndex: (index, className) => { + const element = this.listElements[index]; + if (element) { + element.classList.add(className); + } + }, + focusItemAtIndex: (index) => { + const element = this.listElements[index] as HTMLElement | undefined; + if (element) { + element.focus(); + } + }, + getFocusedElementIndex: () => this.listElements.indexOf(document.activeElement!), + getListItemCount: () => this.listElements.length, + hasCheckboxAtIndex: (index) => { + const listItem = this.listElements[index]; + return !!listItem.querySelector(strings.CHECKBOX_SELECTOR); + }, + hasRadioAtIndex: (index) => { + const listItem = this.listElements[index]; + return !!listItem.querySelector(strings.RADIO_SELECTOR); + }, + isCheckboxCheckedAtIndex: (index) => { + const listItem = this.listElements[index]; + const toggleEl = listItem.querySelector(strings.CHECKBOX_SELECTOR); + return toggleEl!.checked; + }, + isFocusInsideList: () => { + return this.root_.contains(document.activeElement); + }, + notifyAction: (index) => { + this.emit(strings.ACTION_EVENT, {index}, /** shouldBubble */ true); + }, + removeAttributeForElementIndex: (index, attr) => { + const element = this.listElements[index]; + if (element) { + element.removeAttribute(attr); + } + }, + removeClassForElementIndex: (index, className) => { + const element = this.listElements[index]; + if (element) { + element.classList.remove(className); + } + }, + setAttributeForElementIndex: (index, attr, value) => { + const element = this.listElements[index]; + if (element) { + element.setAttribute(attr, value); + } + }, + setCheckedCheckboxOrRadioAtIndex: (index, isChecked) => { + const listItem = this.listElements[index]; + const toggleEl = listItem.querySelector(strings.CHECKBOX_RADIO_SELECTOR); + toggleEl!.checked = isChecked; + + const event = document.createEvent('Event'); + event.initEvent('change', true, true); + toggleEl!.dispatchEvent(event); + }, + setTabIndexForListItemChildren: (listItemIndex, tabIndexValue) => { + const element = this.listElements[listItemIndex]; + const listItemChildren: Element[] = + [].slice.call(element.querySelectorAll(strings.CHILD_ELEMENTS_TO_TOGGLE_TABINDEX)); + listItemChildren.forEach((el) => el.setAttribute('tabindex', tabIndexValue)); + }, + }; + return new MDCListFoundation(adapter); + } + + /** + * Used to figure out which list item this event is targetting. Or returns -1 if + * there is no list item + */ + private getListItemIndex_(evt: Event) { + const eventTarget = evt.target as Element; + const nearestParent = ponyfill.closest(eventTarget, `.${cssClasses.LIST_ITEM_CLASS}, .${cssClasses.ROOT}`); + + // Get the index of the element if it is a list item. + if (nearestParent && ponyfill.matches(nearestParent, `.${cssClasses.LIST_ITEM_CLASS}`)) { + return this.listElements.indexOf(nearestParent); + } + + return -1; + } + + /** + * Used to figure out which element was clicked before sending the event to the foundation. + */ + private handleFocusInEvent_(evt: FocusEvent) { + const index = this.getListItemIndex_(evt); + this.foundation_.handleFocusIn(evt, index); + } + + /** + * Used to figure out which element was clicked before sending the event to the foundation. + */ + private handleFocusOutEvent_(evt: FocusEvent) { + const index = this.getListItemIndex_(evt); + this.foundation_.handleFocusOut(evt, index); + } + + /** + * Used to figure out which element was focused when keydown event occurred before sending the event to the + * foundation. + */ + private handleKeydownEvent_(evt: KeyboardEvent) { + const index = this.getListItemIndex_(evt); + const target = evt.target as Element; + + if (index >= 0) { + this.foundation_.handleKeydown(evt, target.classList.contains(cssClasses.LIST_ITEM_CLASS), index); + } + } + + /** + * Used to figure out which element was clicked before sending the event to the foundation. + */ + private handleClickEvent_(evt: MouseEvent) { + const index = this.getListItemIndex_(evt); + const target = evt.target as Element; + + // Toggle the checkbox only if it's not the target of the event, or the checkbox will have 2 change events. + const toggleCheckbox = !ponyfill.matches(target, strings.CHECKBOX_RADIO_SELECTOR); + this.foundation_.handleClick(index, toggleCheckbox); + } +} diff --git a/packages/mdc-list/foundation.ts b/packages/mdc-list/foundation.ts index 3b4f4f7d37e..bbfa20a0354 100644 --- a/packages/mdc-list/foundation.ts +++ b/packages/mdc-list/foundation.ts @@ -24,15 +24,15 @@ import {MDCFoundation} from '@material/base/foundation'; import {MDCListAdapter} from './adapter'; import {cssClasses, strings} from './constants'; -import {ListIndex} from './types'; +import {MDCListIndex} from './types'; const ELEMENTS_KEY_ALLOWED_IN = ['input', 'button', 'textarea', 'select']; -function isNumberArray(selectedIndex: ListIndex): selectedIndex is number[] { +function isNumberArray(selectedIndex: MDCListIndex): selectedIndex is number[] { return selectedIndex instanceof Array; } -class MDCListFoundation extends MDCFoundation { +export class MDCListFoundation extends MDCFoundation { static get strings() { return strings; } @@ -63,7 +63,7 @@ class MDCListFoundation extends MDCFoundation { private wrapFocus_ = false; private isVertical_ = true; private isSingleSelectionList_ = false; - private selectedIndex_: ListIndex = -1; + private selectedIndex_: MDCListIndex = -1; private focusedItemIndex_ = -1; private useActivatedClass_ = false; private isCheckboxList_ = false; @@ -111,11 +111,11 @@ class MDCListFoundation extends MDCFoundation { this.useActivatedClass_ = useActivated; } - getSelectedIndex(): ListIndex { + getSelectedIndex(): MDCListIndex { return this.selectedIndex_; } - setSelectedIndex(index: ListIndex) { + setSelectedIndex(index: MDCListIndex) { if (!this.isIndexValid_(index)) { return; } @@ -373,7 +373,7 @@ class MDCListFoundation extends MDCFoundation { this.setTabindexAtIndex_(targetIndex); } - private isIndexValid_(index: ListIndex) { + private isIndexValid_(index: MDCListIndex) { if (index instanceof Array) { if (!this.isCheckboxList_) { throw new Error('MDCListFoundation: Array of index is only supported for checkbox based list'); @@ -430,4 +430,4 @@ class MDCListFoundation extends MDCFoundation { } } -export {MDCListFoundation as default, MDCListFoundation}; +export default MDCListFoundation; diff --git a/packages/mdc-list/index.ts b/packages/mdc-list/index.ts index 5bcc61eeda5..37eb9683f7f 100644 --- a/packages/mdc-list/index.ts +++ b/packages/mdc-list/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2018 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,241 +21,7 @@ * THE SOFTWARE. */ -import {MDCComponent} from '@material/base/component'; -import {SpecificEventListener} from '@material/base/index'; -import {ponyfill} from '@material/dom/index'; -import {cssClasses, strings} from './constants'; -import {MDCListFoundation} from './foundation'; -import {ListActionEventDetail, ListIndex} from './types'; - -class MDCList extends MDCComponent { - set vertical(value: boolean) { - this.foundation_.setVerticalOrientation(value); - } - - get listElements(): Element[] { - return [].slice.call(this.root_.querySelectorAll(strings.ENABLED_ITEMS_SELECTOR)); - } - - set wrapFocus(value: boolean) { - this.foundation_.setWrapFocus(value); - } - - set singleSelection(isSingleSelectionList: boolean) { - this.foundation_.setSingleSelection(isSingleSelectionList); - } - - get selectedIndex(): ListIndex { - return this.foundation_.getSelectedIndex(); - } - - set selectedIndex(index: ListIndex) { - this.foundation_.setSelectedIndex(index); - } - - static attachTo(root: Element) { - return new MDCList(root); - } - - private handleKeydown_!: SpecificEventListener<'keydown'>; // assigned in initialSyncWithDOM() - private handleClick_!: SpecificEventListener<'click'>; // assigned in initialSyncWithDOM() - private focusInEventListener_!: SpecificEventListener<'focus'>; // assigned in initialSyncWithDOM() - private focusOutEventListener_!: SpecificEventListener<'focus'>; // assigned in initialSyncWithDOM() - - initialSyncWithDOM() { - this.handleClick_ = this.handleClickEvent_.bind(this); - this.handleKeydown_ = this.handleKeydownEvent_.bind(this); - this.focusInEventListener_ = this.handleFocusInEvent_.bind(this); - this.focusOutEventListener_ = this.handleFocusOutEvent_.bind(this); - this.listen('keydown', this.handleKeydown_); - this.listen('click', this.handleClick_); - this.listen('focusin', this.focusInEventListener_); - this.listen('focusout', this.focusOutEventListener_); - this.layout(); - this.initializeListType(); - } - - destroy() { - this.unlisten('keydown', this.handleKeydown_); - this.unlisten('click', this.handleClick_); - this.unlisten('focusin', this.focusInEventListener_); - this.unlisten('focusout', this.focusOutEventListener_); - } - - layout() { - const direction = this.root_.getAttribute(strings.ARIA_ORIENTATION); - this.vertical = direction !== strings.ARIA_ORIENTATION_HORIZONTAL; - - // List items need to have at least tabindex=-1 to be focusable. - [].slice.call(this.root_.querySelectorAll('.mdc-list-item:not([tabindex])')) - .forEach((el: Element) => { - el.setAttribute('tabindex', '-1'); - }); - - // Child button/a elements are not tabbable until the list item is focused. - [].slice.call(this.root_.querySelectorAll(strings.FOCUSABLE_CHILD_ELEMENTS)) - .forEach((el: Element) => el.setAttribute('tabindex', '-1')); - - this.foundation_.layout(); - } - - /** - * Initialize selectedIndex value based on pre-selected checkbox list items, single selection or radio. - */ - initializeListType() { - const checkboxListItems = this.root_.querySelectorAll(strings.ARIA_ROLE_CHECKBOX_SELECTOR); - const singleSelectedListItem = this.root_.querySelector(` - .${cssClasses.LIST_ITEM_ACTIVATED_CLASS}, - .${cssClasses.LIST_ITEM_SELECTED_CLASS} - `); - const radioSelectedListItem = this.root_.querySelector(strings.ARIA_CHECKED_RADIO_SELECTOR); - - if (checkboxListItems.length) { - const preselectedItems = this.root_.querySelectorAll(strings.ARIA_CHECKED_CHECKBOX_SELECTOR); - this.selectedIndex = - [].map.call(preselectedItems, (listItem: Element) => this.listElements.indexOf(listItem)) as number[]; - } else if (singleSelectedListItem) { - if (singleSelectedListItem.classList.contains(cssClasses.LIST_ITEM_ACTIVATED_CLASS)) { - this.foundation_.setUseActivatedClass(true); - } - - this.singleSelection = true; - this.selectedIndex = this.listElements.indexOf(singleSelectedListItem); - } else if (radioSelectedListItem) { - this.selectedIndex = this.listElements.indexOf(radioSelectedListItem); - } - } - - getDefaultFoundation() { - return new MDCListFoundation({ - addClassForElementIndex: (index, className) => { - const element = this.listElements[index]; - if (element) { - element.classList.add(className); - } - }, - focusItemAtIndex: (index) => { - const element = this.listElements[index] as HTMLElement | undefined; - if (element) { - element.focus(); - } - }, - getFocusedElementIndex: () => this.listElements.indexOf(document.activeElement!), - getListItemCount: () => this.listElements.length, - hasCheckboxAtIndex: (index) => { - const listItem = this.listElements[index]; - return !!listItem.querySelector(strings.CHECKBOX_SELECTOR); - }, - hasRadioAtIndex: (index) => { - const listItem = this.listElements[index]; - return !!listItem.querySelector(strings.RADIO_SELECTOR); - }, - isCheckboxCheckedAtIndex: (index) => { - const listItem = this.listElements[index]; - const toggleEl = listItem.querySelector(strings.CHECKBOX_SELECTOR); - return toggleEl!.checked; - }, - isFocusInsideList: () => { - return this.root_.contains(document.activeElement); - }, - notifyAction: (index) => { - this.emit(strings.ACTION_EVENT, {index}, /** shouldBubble */ true); - }, - removeAttributeForElementIndex: (index, attr) => { - const element = this.listElements[index]; - if (element) { - element.removeAttribute(attr); - } - }, - removeClassForElementIndex: (index, className) => { - const element = this.listElements[index]; - if (element) { - element.classList.remove(className); - } - }, - setAttributeForElementIndex: (index, attr, value) => { - const element = this.listElements[index]; - if (element) { - element.setAttribute(attr, value); - } - }, - setCheckedCheckboxOrRadioAtIndex: (index, isChecked) => { - const listItem = this.listElements[index]; - const toggleEl = listItem.querySelector(strings.CHECKBOX_RADIO_SELECTOR); - toggleEl!.checked = isChecked; - - const event = document.createEvent('Event'); - event.initEvent('change', true, true); - toggleEl!.dispatchEvent(event); - }, - setTabIndexForListItemChildren: (listItemIndex, tabIndexValue) => { - const element = this.listElements[listItemIndex]; - const listItemChildren: Element[] = - [].slice.call(element.querySelectorAll(strings.CHILD_ELEMENTS_TO_TOGGLE_TABINDEX)); - listItemChildren.forEach((el) => el.setAttribute('tabindex', tabIndexValue)); - }, - }); - } - - /** - * Used to figure out which list item this event is targetting. Or returns -1 if - * there is no list item - */ - private getListItemIndex_(evt: Event) { - const eventTarget = evt.target as Element; - const nearestParent = ponyfill.closest(eventTarget, `.${cssClasses.LIST_ITEM_CLASS}, .${cssClasses.ROOT}`); - - // Get the index of the element if it is a list item. - if (nearestParent && ponyfill.matches(nearestParent, `.${cssClasses.LIST_ITEM_CLASS}`)) { - return this.listElements.indexOf(nearestParent); - } - - return -1; - } - - /** - * Used to figure out which element was clicked before sending the event to the foundation. - */ - private handleFocusInEvent_(evt: FocusEvent) { - const index = this.getListItemIndex_(evt); - this.foundation_.handleFocusIn(evt, index); - } - - /** - * Used to figure out which element was clicked before sending the event to the foundation. - */ - private handleFocusOutEvent_(evt: FocusEvent) { - const index = this.getListItemIndex_(evt); - this.foundation_.handleFocusOut(evt, index); - } - - /** - * Used to figure out which element was focused when keydown event occurred before sending the event to the - * foundation. - */ - private handleKeydownEvent_(evt: KeyboardEvent) { - const index = this.getListItemIndex_(evt); - const target = evt.target as Element; - - if (index >= 0) { - this.foundation_.handleKeydown(evt, target.classList.contains(cssClasses.LIST_ITEM_CLASS), index); - } - } - - /** - * Used to figure out which element was clicked before sending the event to the foundation. - */ - private handleClickEvent_(evt: MouseEvent) { - const index = this.getListItemIndex_(evt); - const target = evt.target as Element; - - // Toggle the checkbox only if it's not the target of the event, or the checkbox will have 2 change events. - const toggleCheckbox = !ponyfill.matches(target, strings.CHECKBOX_RADIO_SELECTOR); - this.foundation_.handleClick(index, toggleCheckbox); - } -} - -export {MDCList as default, MDCList}; export * from './adapter'; +export * from './component'; export * from './foundation'; export * from './types'; diff --git a/packages/mdc-list/types.ts b/packages/mdc-list/types.ts index dd0c4ba3f5b..b4b1ee024e6 100644 --- a/packages/mdc-list/types.ts +++ b/packages/mdc-list/types.ts @@ -21,16 +21,15 @@ * THE SOFTWARE. */ -export interface ListActionEventDetail { +export interface MDCListActionEventDetail { /** * Index of the list item that was activated. */ index: number; } -export interface ListActionEvent extends Event { - detail: ListActionEventDetail; +export interface MDCListActionEvent extends Event { + detail: MDCListActionEventDetail; } -export type ListActionEventListener = (evt: ListActionEvent) => void; -export type ListIndex = number | number[]; +export type MDCListIndex = number | number[]; diff --git a/packages/mdc-menu-surface/README.md b/packages/mdc-menu-surface/README.md index 029224f6806..bb232947aa1 100644 --- a/packages/mdc-menu-surface/README.md +++ b/packages/mdc-menu-surface/README.md @@ -156,9 +156,9 @@ Constant Name | Description Type Name | Description --- | --- -`MenuDimensions` | Width/height of an element. See [types.ts](./types.ts). -`MenuDistance` | Margin values representing the distance from anchor point that the menu surface should be shown. See [types.ts](./types.ts). -`MenuPoint` | X/Y coordinates. See [types.ts](./types.ts). +`MDCMenuDimensions` | Width/height of an element. See [types.ts](./types.ts). +`MDCMenuDistance` | Margin values representing the distance from anchor point that the menu surface should be shown. See [types.ts](./types.ts). +`MDCMenuPoint` | X/Y coordinates. See [types.ts](./types.ts). ## `MDCMenuSurface` Properties and Methods @@ -170,7 +170,7 @@ Property | Value Type | Description Method Signature | Description --- | --- `setAnchorCorner(Corner) => void` | Proxies to the foundation's `setAnchorCorner(Corner)` method. -`setAnchorMargin(Partial) => void` | Proxies to the foundation's `setAnchorMargin(Partial)` method. +`setAnchorMargin(Partial) => void` | Proxies to the foundation's `setAnchorMargin(Partial)` method. `setFixedPosition(isFixed: boolean) => void` | Adds the `mdc-menu-surface--fixed` class to the `mdc-menu-surface` element. Proxies to the foundation's `setIsHoisted()` and `setFixedPosition()` methods. `setAbsolutePosition(x: number, y: number) => void` | Proxies to the foundation's `setAbsolutePosition(x, y)` method. Used to set the absolute x/y position of the menu on the page. Should only be used when the menu is hoisted to the body. `setMenuSurfaceAnchorElement(element: Element) => void` | Changes the element used as an anchor for `menu-surface` positioning logic. Should be used with conjunction with `hoistMenuToBody()`. @@ -209,12 +209,12 @@ Method Signature | Description `isLastElementFocused() => boolean` | Returns a boolean value indicating if the last focusable element of the menu-surface is focused. `focusFirstElement() => void` | Focuses the first focusable element of the menu-surface. `focusLastElement() => void` | Focuses the last focusable element of the menu-surface. -`getInnerDimensions() => MenuDimensions` | Returns an object with the items container width and height. +`getInnerDimensions() => MDCMenuDimensions` | Returns an object with the items container width and height. `getAnchorDimensions() => ClientRect \| null` | Returns an object with the dimensions and position of the anchor. -`getBodyDimensions() => MenuDimensions` | Returns an object with width and height of the body, in pixels. -`getWindowDimensions() => MenuDimensions` | Returns an object with width and height of the viewport, in pixels. -`getWindowScroll() => MenuPoint` | Returns an object with the amount the body has been scrolled on the `x` and `y` axis. -`setPosition(position: Partial) => void` | Sets the position of the menu surface element. +`getBodyDimensions() => MDCMenuDimensions` | Returns an object with width and height of the body, in pixels. +`getWindowDimensions() => MDCMenuDimensions` | Returns an object with width and height of the viewport, in pixels. +`getWindowScroll() => MDCMenuPoint` | Returns an object with the amount the body has been scrolled on the `x` and `y` axis. +`setPosition(position: Partial) => void` | Sets the position of the menu surface element. `setMaxHeight(value: string) => void` | Sets `max-height` style for the menu surface element. ### `MDCMenuSurfaceFoundation` @@ -222,7 +222,7 @@ Method Signature | Description Method Signature | Description --- | --- `setAnchorCorner(corner: Corner) => void` | Sets the corner that the menu surface will be anchored to. See [constants.ts](./constants.ts) -`setAnchorMargin(margin: Partial) => void` | Sets the distance from the anchor point that the menu surface should be shown. +`setAnchorMargin(margin: Partial) => void` | Sets the distance from the anchor point that the menu surface should be shown. `setIsHoisted(isHoisted: boolean) => void` | Sets whether the menu surface has been hoisted to the body so that the offsets are calculated relative to the page and not the anchor. `setFixedPosition(isFixed: boolean) => void` | Sets whether the menu surface is using fixed positioning. `setAbsolutePosition(x: number, y: number) => void` | Sets the absolute x/y position of the menu. Should only be used when the menu is hoisted or using fixed positioning. diff --git a/packages/mdc-menu-surface/adapter.ts b/packages/mdc-menu-surface/adapter.ts index 3b885cff25b..4a1aa6b8c69 100644 --- a/packages/mdc-menu-surface/adapter.ts +++ b/packages/mdc-menu-surface/adapter.ts @@ -21,7 +21,7 @@ * THE SOFTWARE. */ -import {MenuDimensions, MenuDistance, MenuPoint} from './types'; +import {MDCMenuDimensions, MDCMenuDistance, MDCMenuPoint} from './types'; /** * Defines the shape of the adapter expected by the foundation. @@ -30,7 +30,7 @@ import {MenuDimensions, MenuDistance, MenuPoint} from './types'; * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCMenuSurfaceAdapter { +export interface MDCMenuSurfaceAdapter { addClass(className: string): void; removeClass(className: string): void; hasClass(className: string): boolean; @@ -42,12 +42,12 @@ interface MDCMenuSurfaceAdapter { isLastElementFocused(): boolean; isRtl(): boolean; - getInnerDimensions(): MenuDimensions; + getInnerDimensions(): MDCMenuDimensions; getAnchorDimensions(): ClientRect | null; - getWindowDimensions(): MenuDimensions; - getBodyDimensions(): MenuDimensions; - getWindowScroll(): MenuPoint; - setPosition(position: Partial): void; + getWindowDimensions(): MDCMenuDimensions; + getBodyDimensions(): MDCMenuDimensions; + getWindowScroll(): MDCMenuPoint; + setPosition(position: Partial): void; setMaxHeight(height: string): void; setTransformOrigin(origin: string): void; @@ -69,5 +69,3 @@ interface MDCMenuSurfaceAdapter { /** Emits an event when the menu surface is opened. */ notifyOpen(): void; } - -export {MDCMenuSurfaceAdapter as default, MDCMenuSurfaceAdapter}; diff --git a/packages/mdc-menu-surface/component.ts b/packages/mdc-menu-surface/component.ts new file mode 100644 index 00000000000..066a7fa33e1 --- /dev/null +++ b/packages/mdc-menu-surface/component.ts @@ -0,0 +1,213 @@ +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {SpecificEventListener} from '@material/base/types'; +import {MDCMenuSurfaceAdapter} from './adapter'; +import {Corner, cssClasses, strings} from './constants'; +import {MDCMenuSurfaceFoundation} from './foundation'; +import {MDCMenuDistance} from './types'; +import * as util from './util'; + +type RegisterFunction = () => void; + +export type MDCMenuSurfaceFactory = (el: Element, foundation?: MDCMenuSurfaceFoundation) => MDCMenuSurface; + +export class MDCMenuSurface extends MDCComponent { + static attachTo(root: Element): MDCMenuSurface { + return new MDCMenuSurface(root); + } + + anchorElement: Element | null = null; + + protected root_!: HTMLElement; // assigned in MDCComponent constructor + + private previousFocus_: HTMLElement | SVGElement | null = null; + private firstFocusableElement_: HTMLElement | SVGElement | null = null; + private lastFocusableElement_: HTMLElement | SVGElement | null = null; + + private handleKeydown_!: SpecificEventListener<'keydown'>; // assigned in initialSyncWithDOM() + private handleBodyClick_!: SpecificEventListener<'click'>; // assigned in initialSyncWithDOM() + + private registerBodyClickListener_!: RegisterFunction; // assigned in initialSyncWithDOM() + private deregisterBodyClickListener_!: RegisterFunction; // assigned in initialSyncWithDOM() + + initialSyncWithDOM() { + const parentEl = this.root_.parentElement; + if (parentEl && parentEl.classList.contains(cssClasses.ANCHOR)) { + this.anchorElement = parentEl; + } + + if (this.root_.classList.contains(cssClasses.FIXED)) { + this.setFixedPosition(true); + } + + this.handleKeydown_ = (evt) => this.foundation_.handleKeydown(evt); + this.handleBodyClick_ = (evt) => this.foundation_.handleBodyClick(evt); + + this.registerBodyClickListener_ = () => document.body.addEventListener('click', this.handleBodyClick_); + this.deregisterBodyClickListener_ = () => document.body.removeEventListener('click', this.handleBodyClick_); + + this.listen('keydown', this.handleKeydown_); + this.listen(strings.OPENED_EVENT, this.registerBodyClickListener_); + this.listen(strings.CLOSED_EVENT, this.deregisterBodyClickListener_); + } + + destroy() { + this.unlisten('keydown', this.handleKeydown_); + this.unlisten(strings.OPENED_EVENT, this.registerBodyClickListener_); + this.unlisten(strings.CLOSED_EVENT, this.deregisterBodyClickListener_); + super.destroy(); + } + + get open(): boolean { + return this.foundation_.isOpen(); + } + + set open(value: boolean) { + if (value) { + const focusableElements = this.root_.querySelectorAll(strings.FOCUSABLE_ELEMENTS); + this.firstFocusableElement_ = focusableElements[0] || null; + this.lastFocusableElement_ = focusableElements[focusableElements.length - 1] || null; + this.foundation_.open(); + } else { + this.foundation_.close(); + } + } + + set quickOpen(quickOpen: boolean) { + this.foundation_.setQuickOpen(quickOpen); + } + + /** + * Removes the menu-surface from it's current location and appends it to the + * body to overcome any overflow:hidden issues. + */ + hoistMenuToBody() { + document.body.appendChild(this.root_); + this.setIsHoisted(true); + } + + /** Sets the foundation to use page offsets for an positioning when the menu is hoisted to the body. */ + setIsHoisted(isHoisted: boolean) { + this.foundation_.setIsHoisted(isHoisted); + } + + /** Sets the element that the menu-surface is anchored to. */ + setMenuSurfaceAnchorElement(element: Element) { + this.anchorElement = element; + } + + /** Sets the menu-surface to position: fixed. */ + setFixedPosition(isFixed: boolean) { + if (isFixed) { + this.root_.classList.add(cssClasses.FIXED); + } else { + this.root_.classList.remove(cssClasses.FIXED); + } + + this.foundation_.setFixedPosition(isFixed); + } + + /** Sets the absolute x/y position to position based on. Requires the menu to be hoisted. */ + setAbsolutePosition(x: number, y: number) { + this.foundation_.setAbsolutePosition(x, y); + this.setIsHoisted(true); + } + + /** + * @param corner Default anchor corner alignment of top-left surface corner. + */ + setAnchorCorner(corner: Corner) { + this.foundation_.setAnchorCorner(corner); + } + + setAnchorMargin(margin: Partial) { + this.foundation_.setAnchorMargin(margin); + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + // tslint:disable:object-literal-sort-keys + const adapter: MDCMenuSurfaceAdapter = { + addClass: (className) => this.root_.classList.add(className), + removeClass: (className) => this.root_.classList.remove(className), + hasClass: (className) => this.root_.classList.contains(className), + hasAnchor: () => !!this.anchorElement, + notifyClose: () => this.emit(MDCMenuSurfaceFoundation.strings.CLOSED_EVENT, {}), + notifyOpen: () => this.emit(MDCMenuSurfaceFoundation.strings.OPENED_EVENT, {}), + isElementInContainer: (el) => this.root_.contains(el), + isRtl: () => getComputedStyle(this.root_).getPropertyValue('direction') === 'rtl', + setTransformOrigin: (origin) => { + const propertyName = `${util.getTransformPropertyName(window)}-origin`; + this.root_.style.setProperty(propertyName, origin); + }, + + isFocused: () => document.activeElement === this.root_, + saveFocus: () => { + this.previousFocus_ = document.activeElement as HTMLElement | SVGElement; + }, + restoreFocus: () => { + if (this.root_.contains(document.activeElement)) { + if (this.previousFocus_ && this.previousFocus_.focus) { + this.previousFocus_.focus(); + } + } + }, + isFirstElementFocused: () => + this.firstFocusableElement_ ? this.firstFocusableElement_ === document.activeElement : false, + isLastElementFocused: () => + this.lastFocusableElement_ ? this.lastFocusableElement_ === document.activeElement : false, + focusFirstElement: () => + this.firstFocusableElement_ && this.firstFocusableElement_.focus && this.firstFocusableElement_.focus(), + focusLastElement: () => + this.lastFocusableElement_ && this.lastFocusableElement_.focus && this.lastFocusableElement_.focus(), + + getInnerDimensions: () => { + return {width: this.root_.offsetWidth, height: this.root_.offsetHeight}; + }, + getAnchorDimensions: () => this.anchorElement ? this.anchorElement.getBoundingClientRect() : null, + getWindowDimensions: () => { + return {width: window.innerWidth, height: window.innerHeight}; + }, + getBodyDimensions: () => { + return {width: document.body.clientWidth, height: document.body.clientHeight}; + }, + getWindowScroll: () => { + return {x: window.pageXOffset, y: window.pageYOffset}; + }, + setPosition: (position) => { + this.root_.style.left = 'left' in position ? `${position.left}px` : ''; + this.root_.style.right = 'right' in position ? `${position.right}px` : ''; + this.root_.style.top = 'top' in position ? `${position.top}px` : ''; + this.root_.style.bottom = 'bottom' in position ? `${position.bottom}px` : ''; + }, + setMaxHeight: (height) => { + this.root_.style.maxHeight = height; + }, + }; + // tslint:enable:object-literal-sort-keys + return new MDCMenuSurfaceFoundation(adapter); + } +} diff --git a/packages/mdc-menu-surface/foundation.ts b/packages/mdc-menu-surface/foundation.ts index e6f7f727a8d..990e9a5bad1 100644 --- a/packages/mdc-menu-surface/foundation.ts +++ b/packages/mdc-menu-surface/foundation.ts @@ -24,18 +24,18 @@ import {MDCFoundation} from '@material/base/foundation'; import {MDCMenuSurfaceAdapter} from './adapter'; import {Corner, CornerBit, cssClasses, numbers, strings} from './constants'; -import {MenuDimensions, MenuDistance, MenuPoint} from './types'; +import {MDCMenuDimensions, MDCMenuDistance, MDCMenuPoint} from './types'; interface AutoLayoutMeasurements { - anchorSize: MenuDimensions; - bodySize: MenuDimensions; - surfaceSize: MenuDimensions; - viewportDistance: MenuDistance; - viewportSize: MenuDimensions; - windowScroll: MenuPoint; + anchorSize: MDCMenuDimensions; + bodySize: MDCMenuDimensions; + surfaceSize: MDCMenuDimensions; + viewportDistance: MDCMenuDistance; + viewportSize: MDCMenuDimensions; + windowScroll: MDCMenuPoint; } -class MDCMenuSurfaceFoundation extends MDCFoundation { +export class MDCMenuSurfaceFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } @@ -99,10 +99,10 @@ class MDCMenuSurfaceFoundation extends MDCFoundation { private animationRequestId_ = 0; private anchorCorner_: Corner = Corner.TOP_START; - private anchorMargin_: MenuDistance = {top: 0, right: 0, bottom: 0, left: 0}; - private position_: MenuPoint = {x: 0, y: 0}; + private anchorMargin_: MDCMenuDistance = {top: 0, right: 0, bottom: 0, left: 0}; + private position_: MDCMenuPoint = {x: 0, y: 0}; - private dimensions_!: MenuDimensions; // assigned in open() + private dimensions_!: MDCMenuDimensions; // assigned in open() private measurements_!: AutoLayoutMeasurements; // assigned in open() constructor(adapter?: Partial) { @@ -138,7 +138,7 @@ class MDCMenuSurfaceFoundation extends MDCFoundation { /** * @param margin Set of margin values from anchor. */ - setAnchorMargin(margin: Partial) { + setAnchorMargin(margin: Partial) { this.anchorMargin_.top = margin.top || 0; this.anchorMargin_.right = margin.right || 0; this.anchorMargin_.bottom = margin.bottom || 0; @@ -263,7 +263,7 @@ class MDCMenuSurfaceFoundation extends MDCFoundation { const verticalOffset = this.getVerticalOriginOffset_(corner); const {anchorSize, surfaceSize} = this.measurements_; - const position: Partial = { + const position: Partial = { [horizontalAlignment]: horizontalOffset ? horizontalOffset : 0, [verticalAlignment]: verticalOffset ? verticalOffset : 0, }; @@ -333,9 +333,9 @@ class MDCMenuSurfaceFoundation extends MDCFoundation { const isBottomAligned = this.hasBit_(this.anchorCorner_, CornerBit.BOTTOM); const availableTop = isBottomAligned ? viewportDistance.top + anchorSize.height + this.anchorMargin_.bottom - : viewportDistance.top + this.anchorMargin_.top; + : viewportDistance.top + this.anchorMargin_.top; const availableBottom = isBottomAligned ? viewportDistance.bottom - this.anchorMargin_.bottom - : viewportDistance.bottom + anchorSize.height - this.anchorMargin_.top; + : viewportDistance.bottom + anchorSize.height - this.anchorMargin_.top; const topOverflow = surfaceSize.height - availableTop; const bottomOverflow = surfaceSize.height - availableBottom; @@ -347,11 +347,11 @@ class MDCMenuSurfaceFoundation extends MDCFoundation { const isFlipRtl = this.hasBit_(this.anchorCorner_, CornerBit.FLIP_RTL); const avoidHorizontalOverlap = this.hasBit_(this.anchorCorner_, CornerBit.RIGHT); const isAlignedRight = (avoidHorizontalOverlap && !isRtl) || - (!avoidHorizontalOverlap && isFlipRtl && isRtl); + (!avoidHorizontalOverlap && isFlipRtl && isRtl); const availableLeft = isAlignedRight ? viewportDistance.left + anchorSize.width + this.anchorMargin_.right : - viewportDistance.left + this.anchorMargin_.left; + viewportDistance.left + this.anchorMargin_.left; const availableRight = isAlignedRight ? viewportDistance.right - this.anchorMargin_.right : - viewportDistance.right + anchorSize.width - this.anchorMargin_.left; + viewportDistance.right + anchorSize.width - this.anchorMargin_.left; const leftOverflow = surfaceSize.width - availableLeft; const rightOverflow = surfaceSize.width - availableRight; @@ -441,10 +441,10 @@ class MDCMenuSurfaceFoundation extends MDCFoundation { } /** Calculates the offsets for positioning the menu-surface when the menu-surface has been hoisted to the body. */ - private adjustPositionForHoistedElement_(position: Partial) { + private adjustPositionForHoistedElement_(position: Partial) { const {windowScroll, viewportDistance} = this.measurements_; - const props = Object.keys(position) as Array>; + const props = Object.keys(position) as Array>; for (const prop of props) { let value = position[prop] || 0; @@ -500,4 +500,4 @@ class MDCMenuSurfaceFoundation extends MDCFoundation { } } -export {MDCMenuSurfaceFoundation as default, MDCMenuSurfaceFoundation}; +export default MDCMenuSurfaceFoundation; diff --git a/packages/mdc-menu-surface/index.ts b/packages/mdc-menu-surface/index.ts index b9c3771b592..429424d8b70 100644 --- a/packages/mdc-menu-surface/index.ts +++ b/packages/mdc-menu-surface/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2018 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,193 +21,11 @@ * THE SOFTWARE. */ -import {MDCComponent} from '@material/base/component'; -import {SpecificEventListener} from '@material/base/index'; -import {Corner, cssClasses, strings} from './constants'; -import {MDCMenuSurfaceFoundation} from './foundation'; -import {MenuDistance} from './types'; import * as util from './util'; -type RegisterFunction = () => void; - -class MDCMenuSurface extends MDCComponent { - static attachTo(root: Element): MDCMenuSurface { - return new MDCMenuSurface(root); - } - - anchorElement: Element | null = null; - - protected root_!: HTMLElement; // assigned in MDCComponent constructor - - private previousFocus_: HTMLElement | SVGElement | null = null; - private firstFocusableElement_: HTMLElement | SVGElement | null = null; - private lastFocusableElement_: HTMLElement | SVGElement | null = null; - - private handleKeydown_!: SpecificEventListener<'keydown'>; // assigned in initialSyncWithDOM() - private handleBodyClick_!: SpecificEventListener<'click'>; // assigned in initialSyncWithDOM() - - private registerBodyClickListener_!: RegisterFunction; // assigned in initialSyncWithDOM() - private deregisterBodyClickListener_!: RegisterFunction; // assigned in initialSyncWithDOM() - - initialSyncWithDOM() { - const parentEl = this.root_.parentElement; - if (parentEl && parentEl.classList.contains(cssClasses.ANCHOR)) { - this.anchorElement = parentEl; - } - - if (this.root_.classList.contains(cssClasses.FIXED)) { - this.setFixedPosition(true); - } - - this.handleKeydown_ = (evt) => this.foundation_.handleKeydown(evt); - this.handleBodyClick_ = (evt) => this.foundation_.handleBodyClick(evt); - - this.registerBodyClickListener_ = () => document.body.addEventListener('click', this.handleBodyClick_); - this.deregisterBodyClickListener_ = () => document.body.removeEventListener('click', this.handleBodyClick_); - - this.root_.addEventListener('keydown', this.handleKeydown_); - this.root_.addEventListener(strings.OPENED_EVENT, this.registerBodyClickListener_); - this.root_.addEventListener(strings.CLOSED_EVENT, this.deregisterBodyClickListener_); - } - - destroy() { - this.root_.removeEventListener('keydown', this.handleKeydown_); - this.root_.removeEventListener(strings.OPENED_EVENT, this.registerBodyClickListener_); - this.root_.removeEventListener(strings.CLOSED_EVENT, this.deregisterBodyClickListener_); - super.destroy(); - } - - get open(): boolean { - return this.foundation_.isOpen(); - } - - set open(value: boolean) { - if (value) { - const focusableElements = this.root_.querySelectorAll(strings.FOCUSABLE_ELEMENTS); - this.firstFocusableElement_ = focusableElements[0] || null; - this.lastFocusableElement_ = focusableElements[focusableElements.length - 1] || null; - this.foundation_.open(); - } else { - this.foundation_.close(); - } - } - - set quickOpen(quickOpen: boolean) { - this.foundation_.setQuickOpen(quickOpen); - } - - /** - * Removes the menu-surface from it's current location and appends it to the - * body to overcome any overflow:hidden issues. - */ - hoistMenuToBody() { - document.body.appendChild(this.root_); - this.setIsHoisted(true); - } - - /** Sets the foundation to use page offsets for an positioning when the menu is hoisted to the body. */ - setIsHoisted(isHoisted: boolean) { - this.foundation_.setIsHoisted(isHoisted); - } - - /** Sets the element that the menu-surface is anchored to. */ - setMenuSurfaceAnchorElement(element: Element) { - this.anchorElement = element; - } - - /** Sets the menu-surface to position: fixed. */ - setFixedPosition(isFixed: boolean) { - if (isFixed) { - this.root_.classList.add(cssClasses.FIXED); - } else { - this.root_.classList.remove(cssClasses.FIXED); - } - - this.foundation_.setFixedPosition(isFixed); - } - - /** Sets the absolute x/y position to position based on. Requires the menu to be hoisted. */ - setAbsolutePosition(x: number, y: number) { - this.foundation_.setAbsolutePosition(x, y); - this.setIsHoisted(true); - } - - /** - * @param corner Default anchor corner alignment of top-left surface corner. - */ - setAnchorCorner(corner: Corner) { - this.foundation_.setAnchorCorner(corner); - } - - setAnchorMargin(margin: Partial) { - this.foundation_.setAnchorMargin(margin); - } - - getDefaultFoundation(): MDCMenuSurfaceFoundation { - // tslint:disable:object-literal-sort-keys - return new MDCMenuSurfaceFoundation({ - addClass: (className) => this.root_.classList.add(className), - removeClass: (className) => this.root_.classList.remove(className), - hasClass: (className) => this.root_.classList.contains(className), - hasAnchor: () => !!this.anchorElement, - notifyClose: () => this.emit(MDCMenuSurfaceFoundation.strings.CLOSED_EVENT, {}), - notifyOpen: () => this.emit(MDCMenuSurfaceFoundation.strings.OPENED_EVENT, {}), - isElementInContainer: (el) => this.root_.contains(el), - isRtl: () => getComputedStyle(this.root_).getPropertyValue('direction') === 'rtl', - setTransformOrigin: (origin) => { - const propertyName = `${util.getTransformPropertyName(window)}-origin`; - this.root_.style.setProperty(propertyName, origin); - }, - - isFocused: () => document.activeElement === this.root_, - saveFocus: () => { - this.previousFocus_ = document.activeElement as HTMLElement | SVGElement; - }, - restoreFocus: () => { - if (this.root_.contains(document.activeElement)) { - if (this.previousFocus_ && this.previousFocus_.focus) { - this.previousFocus_.focus(); - } - } - }, - isFirstElementFocused: () => - this.firstFocusableElement_ ? this.firstFocusableElement_ === document.activeElement : false, - isLastElementFocused: () => - this.lastFocusableElement_ ? this.lastFocusableElement_ === document.activeElement : false, - focusFirstElement: () => - this.firstFocusableElement_ && this.firstFocusableElement_.focus && this.firstFocusableElement_.focus(), - focusLastElement: () => - this.lastFocusableElement_ && this.lastFocusableElement_.focus && this.lastFocusableElement_.focus(), - - getInnerDimensions: () => { - return {width: this.root_.offsetWidth, height: this.root_.offsetHeight}; - }, - getAnchorDimensions: () => this.anchorElement ? this.anchorElement.getBoundingClientRect() : null, - getWindowDimensions: () => { - return {width: window.innerWidth, height: window.innerHeight}; - }, - getBodyDimensions: () => { - return {width: document.body.clientWidth, height: document.body.clientHeight}; - }, - getWindowScroll: () => { - return {x: window.pageXOffset, y: window.pageYOffset}; - }, - setPosition: (position) => { - this.root_.style.left = 'left' in position ? `${position.left}px` : ''; - this.root_.style.right = 'right' in position ? `${position.right}px` : ''; - this.root_.style.top = 'top' in position ? `${position.top}px` : ''; - this.root_.style.bottom = 'bottom' in position ? `${position.bottom}px` : ''; - }, - setMaxHeight: (height) => { - this.root_.style.maxHeight = height; - }, - }); - // tslint:enable:object-literal-sort-keys - } -} - -export {MDCMenuSurface as default, MDCMenuSurface, util}; export {Corner, CornerBit} from './constants'; +export {util}; export * from './adapter'; +export * from './component'; export * from './foundation'; export * from './types'; diff --git a/packages/mdc-menu-surface/types.ts b/packages/mdc-menu-surface/types.ts index edd2b1f8cd5..736d7dfaeb7 100644 --- a/packages/mdc-menu-surface/types.ts +++ b/packages/mdc-menu-surface/types.ts @@ -21,19 +21,19 @@ * THE SOFTWARE. */ -export interface MenuDimensions { +export interface MDCMenuDimensions { width: number; height: number; } -export interface MenuDistance { +export interface MDCMenuDistance { top: number; right: number; bottom: number; left: number; } -export interface MenuPoint { +export interface MDCMenuPoint { x: number; y: number; } diff --git a/packages/mdc-menu/README.md b/packages/mdc-menu/README.md index 391b322055b..9b5584c7333 100644 --- a/packages/mdc-menu/README.md +++ b/packages/mdc-menu/README.md @@ -204,7 +204,7 @@ Property | Value Type | Description Method Signature | Description --- | --- `setAnchorCorner(Corner) => void` | Proxies to the menu surface's `setAnchorCorner(Corner)` method. -`setAnchorMargin(Partial) => void` | Proxies to the menu surface's `setAnchorMargin(Partial)` method. +`setAnchorMargin(Partial) => void` | Proxies to the menu surface's `setAnchorMargin(Partial)` method. `setAbsolutePosition(x: number, y: number) => void` | Proxies to the menu surface's `setAbsolutePosition(x: number, y: number)` method. `setFixedPosition(isFixed: boolean) => void` | Proxies to the menu surface's `setFixedPosition(isFixed: boolean)` method. `hoistMenuToBody() => void` | Proxies to the menu surface's `hoistMenuToBody()` method. diff --git a/packages/mdc-menu/adapter.ts b/packages/mdc-menu/adapter.ts index c98609570bd..0dce8d260c7 100644 --- a/packages/mdc-menu/adapter.ts +++ b/packages/mdc-menu/adapter.ts @@ -21,7 +21,7 @@ * THE SOFTWARE. */ -import {MenuItemEventDetail} from './types'; +import {MDCMenuItemEventDetail} from './types'; /** * Implement this adapter for your framework of choice to delegate updates to @@ -29,7 +29,7 @@ import {MenuItemEventDetail} from './types'; * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCMenuAdapter { +export interface MDCMenuAdapter { /** * Adds a class to the element at the index provided. */ @@ -78,7 +78,5 @@ interface MDCMenuAdapter { /** * Emit an event when a menu item is selected. */ - notifySelected(evtData: MenuItemEventDetail): void; + notifySelected(evtData: MDCMenuItemEventDetail): void; } - -export {MDCMenuAdapter as default, MDCMenuAdapter}; diff --git a/packages/mdc-menu/component.ts b/packages/mdc-menu/component.ts new file mode 100644 index 00000000000..6b82c0465d3 --- /dev/null +++ b/packages/mdc-menu/component.ts @@ -0,0 +1,213 @@ +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {CustomEventListener, SpecificEventListener} from '@material/base/types'; +import {MDCList, MDCListActionEvent, MDCListFactory, MDCListFoundation} from '@material/list/index'; +import {MDCMenuSurfaceFoundation} from '@material/menu-surface/foundation'; +import {Corner, MDCMenuSurface, MDCMenuSurfaceFactory} from '@material/menu-surface/index'; +import {MDCMenuDistance} from '@material/menu-surface/types'; +import {MDCMenuAdapter} from './adapter'; +import {cssClasses, strings} from './constants'; +import {MDCMenuFoundation} from './foundation'; +import {MDCMenuItemComponentEventDetail} from './types'; + +export type MDCMenuFactory = (el: Element, foundation?: MDCMenuFoundation) => MDCMenu; + +export class MDCMenu extends MDCComponent { + static attachTo(root: Element) { + return new MDCMenu(root); + } + + private menuSurfaceFactory_!: MDCMenuSurfaceFactory; // assigned in initialize() + private listFactory_!: MDCListFactory; // assigned in initialize() + + private menuSurface_!: MDCMenuSurface; // assigned in initialSyncWithDOM() + private list_!: MDCList | null; // assigned in initialSyncWithDOM() + + private handleKeydown_!: SpecificEventListener<'keydown'>; // assigned in initialSyncWithDOM() + private handleItemAction_!: CustomEventListener; // assigned in initialSyncWithDOM() + private afterOpenedCallback_!: EventListener; // assigned in initialSyncWithDOM() + + initialize( + menuSurfaceFactory: MDCMenuSurfaceFactory = (el) => new MDCMenuSurface(el), + listFactory: MDCListFactory = (el) => new MDCList(el)) { + this.menuSurfaceFactory_ = menuSurfaceFactory; + this.listFactory_ = listFactory; + } + + initialSyncWithDOM() { + this.menuSurface_ = this.menuSurfaceFactory_(this.root_); + + const list = this.root_.querySelector(strings.LIST_SELECTOR); + if (list) { + this.list_ = this.listFactory_(list); + this.list_.wrapFocus = true; + } else { + this.list_ = null; + } + + this.handleKeydown_ = (evt) => this.foundation_.handleKeydown(evt); + this.handleItemAction_ = (evt) => this.foundation_.handleItemAction(this.items[evt.detail.index]); + this.afterOpenedCallback_ = () => this.handleAfterOpened_(); + + this.menuSurface_.listen(MDCMenuSurfaceFoundation.strings.OPENED_EVENT, this.afterOpenedCallback_); + this.listen('keydown', this.handleKeydown_); + this.listen(MDCListFoundation.strings.ACTION_EVENT, this.handleItemAction_); + } + + destroy() { + if (this.list_) { + this.list_.destroy(); + } + + this.menuSurface_.destroy(); + this.menuSurface_.unlisten(MDCMenuSurfaceFoundation.strings.OPENED_EVENT, this.afterOpenedCallback_); + this.unlisten('keydown', this.handleKeydown_); + this.unlisten(MDCListFoundation.strings.ACTION_EVENT, this.handleItemAction_); + super.destroy(); + } + + get open(): boolean { + return this.menuSurface_.open; + } + + set open(value: boolean) { + this.menuSurface_.open = value; + } + + get wrapFocus(): boolean { + return this.list_ ? this.list_.wrapFocus : false; + } + + set wrapFocus(value: boolean) { + if (this.list_) { + this.list_.wrapFocus = value; + } + } + + /** + * Return the items within the menu. Note that this only contains the set of elements within + * the items container that are proper list items, and not supplemental / presentational DOM + * elements. + */ + get items(): Element[] { + return this.list_ ? this.list_.listElements : []; + } + + set quickOpen(quickOpen: boolean) { + this.menuSurface_.quickOpen = quickOpen; + } + + /** + * @param corner Default anchor corner alignment of top-left menu corner. + */ + setAnchorCorner(corner: Corner) { + this.menuSurface_.setAnchorCorner(corner); + } + + setAnchorMargin(margin: Partial) { + this.menuSurface_.setAnchorMargin(margin); + } + + /** + * @return The item within the menu at the index specified. + */ + getOptionByIndex(index: number): Element | null { + const items = this.items; + + if (index < items.length) { + return this.items[index]; + } else { + return null; + } + } + + setFixedPosition(isFixed: boolean) { + this.menuSurface_.setFixedPosition(isFixed); + } + + hoistMenuToBody() { + this.menuSurface_.hoistMenuToBody(); + } + + setIsHoisted(isHoisted: boolean) { + this.menuSurface_.setIsHoisted(isHoisted); + } + + setAbsolutePosition(x: number, y: number) { + this.menuSurface_.setAbsolutePosition(x, y); + } + + /** + * Sets the element that the menu-surface is anchored to. + */ + setAnchorElement(element: Element) { + this.menuSurface_.anchorElement = element; + } + + handleAfterOpened_() { + const list = this.items; + if (list.length > 0) { + (list[0] as HTMLElement).focus(); + } + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + // tslint:disable:object-literal-sort-keys + const adapter: MDCMenuAdapter = { + addClassToElementAtIndex: (index, className) => { + const list = this.items; + list[index].classList.add(className); + }, + removeClassFromElementAtIndex: (index, className) => { + const list = this.items; + list[index].classList.remove(className); + }, + addAttributeToElementAtIndex: (index, attr, value) => { + const list = this.items; + list[index].setAttribute(attr, value); + }, + removeAttributeFromElementAtIndex: (index, attr) => { + const list = this.items; + list[index].removeAttribute(attr); + }, + elementContainsClass: (element, className) => element.classList.contains(className), + closeSurface: () => this.open = false, + getElementIndex: (element) => this.items.indexOf(element), + getParentElement: (element) => element.parentElement, + getSelectedElementIndex: (selectionGroup) => { + const selectedListItem = selectionGroup.querySelector(`.${cssClasses.MENU_SELECTED_LIST_ITEM}`); + return selectedListItem ? this.items.indexOf(selectedListItem) : -1; + }, + notifySelected: (evtData) => this.emit(strings.SELECTED_EVENT, { + index: evtData.index, + item: this.items[evtData.index], + }), + }; + // tslint:enable:object-literal-sort-keys + return new MDCMenuFoundation(adapter); + } +} diff --git a/packages/mdc-menu/foundation.ts b/packages/mdc-menu/foundation.ts index 41443ef1ae2..cbe84a09310 100644 --- a/packages/mdc-menu/foundation.ts +++ b/packages/mdc-menu/foundation.ts @@ -27,7 +27,7 @@ import {MDCMenuSurfaceFoundation} from '@material/menu-surface/foundation'; import {MDCMenuAdapter} from './adapter'; import {cssClasses, strings} from './constants'; -class MDCMenuFoundation extends MDCFoundation { +export class MDCMenuFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } @@ -137,4 +137,4 @@ class MDCMenuFoundation extends MDCFoundation { } } -export {MDCMenuFoundation as default, MDCMenuFoundation}; +export default MDCMenuFoundation; diff --git a/packages/mdc-menu/index.ts b/packages/mdc-menu/index.ts index c371037b248..81819a6a4d1 100644 --- a/packages/mdc-menu/index.ts +++ b/packages/mdc-menu/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2018 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,192 +21,8 @@ * THE SOFTWARE. */ -import {MDCComponent} from '@material/base/component'; -import {CustomEventListener, SpecificEventListener} from '@material/base/index'; -import {ListActionEvent, MDCList, MDCListFoundation} from '@material/list/index'; -import {MDCMenuSurfaceFoundation} from '@material/menu-surface/foundation'; -import {Corner, MDCMenuSurface} from '@material/menu-surface/index'; -import {MenuDistance} from '@material/menu-surface/types'; -import {cssClasses, strings} from './constants'; -import {MDCMenuFoundation} from './foundation'; -import {DefaultMenuItemEventDetail, ListFactory, MenuSurfaceFactory} from './types'; - -class MDCMenu extends MDCComponent { - static attachTo(root: Element) { - return new MDCMenu(root); - } - - private menuSurfaceFactory_!: MenuSurfaceFactory; // assigned in initialize() - private listFactory_!: ListFactory; // assigned in initialize() - - private menuSurface_!: MDCMenuSurface; // assigned in initialSyncWithDOM() - private list_!: MDCList | null; // assigned in initialSyncWithDOM() - - private handleKeydown_!: SpecificEventListener<'keydown'>; // assigned in initialSyncWithDOM() - private handleItemAction_!: CustomEventListener; // assigned in initialSyncWithDOM() - private afterOpenedCallback_!: EventListener; // assigned in initialSyncWithDOM() - - initialize( - menuSurfaceFactory: MenuSurfaceFactory = (el) => new MDCMenuSurface(el), - listFactory: ListFactory = (el) => new MDCList(el)) { - this.menuSurfaceFactory_ = menuSurfaceFactory; - this.listFactory_ = listFactory; - } - - initialSyncWithDOM() { - this.menuSurface_ = this.menuSurfaceFactory_(this.root_); - - const list = this.root_.querySelector(strings.LIST_SELECTOR); - if (list) { - this.list_ = this.listFactory_(list); - this.list_.wrapFocus = true; - } else { - this.list_ = null; - } - - this.handleKeydown_ = (evt) => this.foundation_.handleKeydown(evt); - this.handleItemAction_ = (evt) => this.foundation_.handleItemAction(this.items[evt.detail.index]); - this.afterOpenedCallback_ = () => this.handleAfterOpened_(); - - this.menuSurface_.listen(MDCMenuSurfaceFoundation.strings.OPENED_EVENT, this.afterOpenedCallback_); - this.listen('keydown', this.handleKeydown_); - this.listen(MDCListFoundation.strings.ACTION_EVENT, this.handleItemAction_); - } - - destroy() { - if (this.list_) { - this.list_.destroy(); - } - - this.menuSurface_.destroy(); - this.menuSurface_.unlisten(MDCMenuSurfaceFoundation.strings.OPENED_EVENT, this.afterOpenedCallback_); - this.unlisten('keydown', this.handleKeydown_); - this.unlisten(MDCListFoundation.strings.ACTION_EVENT, this.handleItemAction_); - super.destroy(); - } - - get open(): boolean { - return this.menuSurface_.open; - } - - set open(value: boolean) { - this.menuSurface_.open = value; - } - - get wrapFocus(): boolean { - return this.list_ ? this.list_.wrapFocus : false; - } - - set wrapFocus(value: boolean) { - if (this.list_) { - this.list_.wrapFocus = value; - } - } - - /** - * Return the items within the menu. Note that this only contains the set of elements within - * the items container that are proper list items, and not supplemental / presentational DOM - * elements. - */ - get items(): Element[] { - return this.list_ ? this.list_.listElements : []; - } - - set quickOpen(quickOpen: boolean) { - this.menuSurface_.quickOpen = quickOpen; - } - - /** - * @param corner Default anchor corner alignment of top-left menu corner. - */ - setAnchorCorner(corner: Corner) { - this.menuSurface_.setAnchorCorner(corner); - } - - setAnchorMargin(margin: Partial) { - this.menuSurface_.setAnchorMargin(margin); - } - - /** - * @return The item within the menu at the index specified. - */ - getOptionByIndex(index: number): Element | null { - const items = this.items; - - if (index < items.length) { - return this.items[index]; - } else { - return null; - } - } - - setFixedPosition(isFixed: boolean) { - this.menuSurface_.setFixedPosition(isFixed); - } - - hoistMenuToBody() { - this.menuSurface_.hoistMenuToBody(); - } - - setIsHoisted(isHoisted: boolean) { - this.menuSurface_.setIsHoisted(isHoisted); - } - - setAbsolutePosition(x: number, y: number) { - this.menuSurface_.setAbsolutePosition(x, y); - } - - /** - * Sets the element that the menu-surface is anchored to. - */ - setAnchorElement(element: Element) { - this.menuSurface_.anchorElement = element; - } - - handleAfterOpened_() { - const list = this.items; - if (list.length > 0) { - (list[0] as HTMLElement).focus(); - } - } - - getDefaultFoundation(): MDCMenuFoundation { - // tslint:disable:object-literal-sort-keys - return new MDCMenuFoundation({ - addClassToElementAtIndex: (index, className) => { - const list = this.items; - list[index].classList.add(className); - }, - removeClassFromElementAtIndex: (index, className) => { - const list = this.items; - list[index].classList.remove(className); - }, - addAttributeToElementAtIndex: (index, attr, value) => { - const list = this.items; - list[index].setAttribute(attr, value); - }, - removeAttributeFromElementAtIndex: (index, attr) => { - const list = this.items; - list[index].removeAttribute(attr); - }, - elementContainsClass: (element, className) => element.classList.contains(className), - closeSurface: () => this.open = false, - getElementIndex: (element) => this.items.indexOf(element), - getParentElement: (element) => element.parentElement, - getSelectedElementIndex: (selectionGroup) => { - const selectedListItem = selectionGroup.querySelector(`.${cssClasses.MENU_SELECTED_LIST_ITEM}`); - return selectedListItem ? this.items.indexOf(selectedListItem) : -1; - }, - notifySelected: (evtData) => this.emit(strings.SELECTED_EVENT, { - index: evtData.index, - item: this.items[evtData.index], - }), - }); - // tslint:enable:object-literal-sort-keys - } -} - -export {MDCMenu as default, MDCMenu, Corner}; +export {Corner} from '@material/menu-surface/constants'; // for backward compatibility export * from './adapter'; +export * from './component'; export * from './foundation'; export * from './types'; diff --git a/packages/mdc-menu/types.ts b/packages/mdc-menu/types.ts index e40d4421ae7..1fe25f0e6a1 100644 --- a/packages/mdc-menu/types.ts +++ b/packages/mdc-menu/types.ts @@ -21,25 +21,26 @@ * THE SOFTWARE. */ -import {MDCList} from '@material/list/index'; -import {MDCMenuSurface} from '@material/menu-surface/index'; - -export type MenuItemEvent = CustomEvent; -export type DefaultMenuItemEvent = CustomEvent; - /** * Event properties used by the adapter and foundation. */ -export interface MenuItemEventDetail { +export interface MDCMenuItemEventDetail { index: number; } /** * Event properties specific to the default component implementation. */ -export interface DefaultMenuItemEventDetail extends MenuItemEventDetail { +export interface MDCMenuItemComponentEventDetail extends MDCMenuItemEventDetail { item: Element; } -export type MenuSurfaceFactory = (el: Element) => MDCMenuSurface; -export type ListFactory = (el: Element) => MDCList; +// Note: CustomEvent is not supported by Closure Compiler. + +export interface MDCMenuItemEvent extends Event { + readonly detail: MDCMenuItemEventDetail; +} + +export interface MDCMenuItemComponentEvent extends Event { + readonly detail: MDCMenuItemComponentEventDetail; +} diff --git a/packages/mdc-notched-outline/adapter.ts b/packages/mdc-notched-outline/adapter.ts index 2546f126091..cd147fa9762 100644 --- a/packages/mdc-notched-outline/adapter.ts +++ b/packages/mdc-notched-outline/adapter.ts @@ -28,7 +28,7 @@ * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCNotchedOutlineAdapter { +export interface MDCNotchedOutlineAdapter { /** * Adds a class to the root element. */ @@ -49,5 +49,3 @@ interface MDCNotchedOutlineAdapter { */ removeNotchWidthProperty(): void; } - -export {MDCNotchedOutlineAdapter as default, MDCNotchedOutlineAdapter}; diff --git a/packages/mdc-notched-outline/component.ts b/packages/mdc-notched-outline/component.ts new file mode 100644 index 00000000000..d0cc2d9b82e --- /dev/null +++ b/packages/mdc-notched-outline/component.ts @@ -0,0 +1,82 @@ +/** + * @license + * Copyright 2017 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {MDCFloatingLabelFoundation} from '@material/floating-label/foundation'; +import {MDCNotchedOutlineAdapter} from './adapter'; +import {cssClasses, strings} from './constants'; +import {MDCNotchedOutlineFoundation} from './foundation'; + +export type MDCNotchedOutlineFactory = (el: Element, foundation?: MDCNotchedOutlineFoundation) => MDCNotchedOutline; + +export class MDCNotchedOutline extends MDCComponent { + static attachTo(root: Element): MDCNotchedOutline { + return new MDCNotchedOutline(root); + } + + private notchElement_!: HTMLElement; // assigned in initialSyncWithDOM() + + initialSyncWithDOM() { + this.notchElement_ = this.root_.querySelector(strings.NOTCH_ELEMENT_SELECTOR)!; + + const label = this.root_.querySelector('.' + MDCFloatingLabelFoundation.cssClasses.ROOT); + if (label) { + label.style.transitionDuration = '0s'; + this.root_.classList.add(cssClasses.OUTLINE_UPGRADED); + requestAnimationFrame(() => { + label.style.transitionDuration = ''; + }); + } else { + this.root_.classList.add(cssClasses.NO_LABEL); + } + } + + /** + * Updates classes and styles to open the notch to the specified width. + * @param notchWidth The notch width in the outline. + */ + notch(notchWidth: number) { + this.foundation_.notch(notchWidth); + } + + /** + * Updates classes and styles to close the notch. + */ + closeNotch() { + this.foundation_.closeNotch(); + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + // tslint:disable:object-literal-sort-keys + const adapter: MDCNotchedOutlineAdapter = { + addClass: (className) => this.root_.classList.add(className), + removeClass: (className) => this.root_.classList.remove(className), + setNotchWidthProperty: (width) => this.notchElement_.style.setProperty('width', width + 'px'), + removeNotchWidthProperty: () => this.notchElement_.style.removeProperty('width'), + }; + // tslint:enable:object-literal-sort-keys + return new MDCNotchedOutlineFoundation(adapter); + } +} diff --git a/packages/mdc-notched-outline/foundation.ts b/packages/mdc-notched-outline/foundation.ts index e8793e4a3c4..3e100c70e54 100644 --- a/packages/mdc-notched-outline/foundation.ts +++ b/packages/mdc-notched-outline/foundation.ts @@ -25,7 +25,7 @@ import {MDCFoundation} from '@material/base/foundation'; import {MDCNotchedOutlineAdapter} from './adapter'; import {cssClasses, numbers, strings} from './constants'; -class MDCNotchedOutlineFoundation extends MDCFoundation { +export class MDCNotchedOutlineFoundation extends MDCFoundation { static get strings() { return strings; } @@ -80,4 +80,4 @@ class MDCNotchedOutlineFoundation extends MDCFoundation { - static attachTo(root: Element): MDCNotchedOutline { - return new MDCNotchedOutline(root); - } - - private notchElement_!: HTMLElement; // assigned in initialSyncWithDOM() - - initialSyncWithDOM() { - this.notchElement_ = this.root_.querySelector(strings.NOTCH_ELEMENT_SELECTOR)!; - - const label = this.root_.querySelector('.' + MDCFloatingLabelFoundation.cssClasses.ROOT); - if (label) { - label.style.transitionDuration = '0s'; - this.root_.classList.add(cssClasses.OUTLINE_UPGRADED); - requestAnimationFrame(() => { - label.style.transitionDuration = ''; - }); - } else { - this.root_.classList.add(cssClasses.NO_LABEL); - } - } - - /** - * Updates classes and styles to open the notch to the specified width. - * @param notchWidth The notch width in the outline. - */ - notch(notchWidth: number) { - this.foundation_.notch(notchWidth); - } - - /** - * Updates classes and styles to close the notch. - */ - closeNotch() { - this.foundation_.closeNotch(); - } - - getDefaultFoundation(): MDCNotchedOutlineFoundation { - // tslint:disable:object-literal-sort-keys - return new MDCNotchedOutlineFoundation({ - addClass: (className) => this.root_.classList.add(className), - removeClass: (className) => this.root_.classList.remove(className), - setNotchWidthProperty: (width) => this.notchElement_.style.setProperty('width', width + 'px'), - removeNotchWidthProperty: () => this.notchElement_.style.removeProperty('width'), - }); - // tslint:enable:object-literal-sort-keys - } -} - -export {MDCNotchedOutline as default, MDCNotchedOutline}; export * from './adapter'; +export * from './component'; export * from './foundation'; diff --git a/packages/mdc-radio/adapter.ts b/packages/mdc-radio/adapter.ts index af843bd15a1..d85fa72a7a8 100644 --- a/packages/mdc-radio/adapter.ts +++ b/packages/mdc-radio/adapter.ts @@ -28,10 +28,8 @@ * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCRadioAdapter { +export interface MDCRadioAdapter { addClass(className: string): void; removeClass(className: string): void; setNativeControlDisabled(disabled: boolean): void; } - -export {MDCRadioAdapter as default, MDCRadioAdapter}; diff --git a/packages/mdc-radio/component.ts b/packages/mdc-radio/component.ts new file mode 100644 index 00000000000..1747653bcaa --- /dev/null +++ b/packages/mdc-radio/component.ts @@ -0,0 +1,112 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {MDCRipple, MDCRippleAdapter, MDCRippleCapableSurface, MDCRippleFoundation} from '@material/ripple/index'; +import {MDCSelectionControl} from '@material/selection-control/index'; +import {MDCRadioAdapter} from './adapter'; +import {MDCRadioFoundation} from './foundation'; + +export class MDCRadio extends MDCComponent implements MDCRippleCapableSurface, MDCSelectionControl { + static attachTo(root: Element) { + return new MDCRadio(root); + } + + // Public visibility for this property is required by MDCRippleCapableSurface. + root_!: Element; // assigned in MDCComponent constructor + + private readonly ripple_: MDCRipple = this.createRipple_(); + + get checked(): boolean { + return this.nativeControl_.checked; + } + + set checked(checked: boolean) { + this.nativeControl_.checked = checked; + } + + get disabled() { + return this.nativeControl_.disabled; + } + + set disabled(disabled: boolean) { + this.foundation_.setDisabled(disabled); + } + + get value() { + return this.nativeControl_.value; + } + + set value(value: string) { + this.nativeControl_.value = value; + } + + get ripple(): MDCRipple { + return this.ripple_; + } + + destroy() { + this.ripple_.destroy(); + super.destroy(); + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + const adapter: MDCRadioAdapter = { + addClass: (className) => this.root_.classList.add(className), + removeClass: (className) => this.root_.classList.remove(className), + setNativeControlDisabled: (disabled) => this.nativeControl_.disabled = disabled, + }; + return new MDCRadioFoundation(adapter); + } + + private createRipple_(): MDCRipple { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + // tslint:disable:object-literal-sort-keys + const adapter: MDCRippleAdapter = { + ...MDCRipple.createAdapter(this), + registerInteractionHandler: (evtType, handler) => this.nativeControl_.addEventListener(evtType, handler), + deregisterInteractionHandler: (evtType, handler) => this.nativeControl_.removeEventListener(evtType, handler), + // Radio buttons technically go "active" whenever there is *any* keyboard interaction. + // This is not the UI we desire. + isSurfaceActive: () => false, + isUnbounded: () => true, + }; + // tslint:enable:object-literal-sort-keys + return new MDCRipple(this.root_, new MDCRippleFoundation(adapter)); + } + + /** + * Returns the state of the native control element, or null if the native control element is not present. + */ + private get nativeControl_(): HTMLInputElement { + const {NATIVE_CONTROL_SELECTOR} = MDCRadioFoundation.strings; + const el = this.root_.querySelector(NATIVE_CONTROL_SELECTOR); + if (!el) { + throw new Error(`Radio component requires a ${NATIVE_CONTROL_SELECTOR} element`); + } + return el; + } +} diff --git a/packages/mdc-radio/foundation.ts b/packages/mdc-radio/foundation.ts index 64387075eba..318573e12db 100644 --- a/packages/mdc-radio/foundation.ts +++ b/packages/mdc-radio/foundation.ts @@ -25,7 +25,7 @@ import {MDCFoundation} from '@material/base/foundation'; import {MDCRadioAdapter} from './adapter'; import {cssClasses, strings} from './constants'; -class MDCRadioFoundation extends MDCFoundation { +export class MDCRadioFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } @@ -42,6 +42,10 @@ class MDCRadioFoundation extends MDCFoundation { }; } + constructor(adapter?: Partial) { + super({...MDCRadioFoundation.defaultAdapter, ...adapter}); + } + setDisabled(disabled: boolean) { const {DISABLED} = MDCRadioFoundation.cssClasses; this.adapter_.setNativeControlDisabled(disabled); @@ -53,4 +57,4 @@ class MDCRadioFoundation extends MDCFoundation { } } -export {MDCRadioFoundation as default, MDCRadioFoundation}; +export default MDCRadioFoundation; diff --git a/packages/mdc-radio/index.ts b/packages/mdc-radio/index.ts index 280ad4508a5..f8c89ac94f3 100644 --- a/packages/mdc-radio/index.ts +++ b/packages/mdc-radio/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2016 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,92 +21,6 @@ * THE SOFTWARE. */ -import {MDCComponent} from '@material/base/component'; -import {EventType, SpecificEventListener} from '@material/base/index'; -import {MDCRipple, MDCRippleFoundation, RippleCapableSurface} from '@material/ripple/index'; -import {MDCSelectionControl} from '@material/selection-control/index'; - -import {MDCRadioFoundation} from './foundation'; - -class MDCRadio extends MDCComponent implements RippleCapableSurface, MDCSelectionControl { - static attachTo(root: Element) { - return new MDCRadio(root); - } - - // Public visibility for this property is required by RippleCapableSurface. - root_!: Element; // assigned in MDCComponent constructor - - private readonly ripple_: MDCRipple = this.initRipple_(); - - get checked(): boolean { - return this.nativeControl_.checked; - } - - set checked(checked: boolean) { - this.nativeControl_.checked = checked; - } - - get disabled() { - return this.nativeControl_.disabled; - } - - set disabled(disabled: boolean) { - this.foundation_.setDisabled(disabled); - } - - get value() { - return this.nativeControl_.value; - } - - set value(value: string) { - this.nativeControl_.value = value; - } - - get ripple(): MDCRipple { - return this.ripple_; - } - - destroy() { - this.ripple_.destroy(); - super.destroy(); - } - - getDefaultFoundation() { - return new MDCRadioFoundation({ - addClass: (className) => this.root_.classList.add(className), - removeClass: (className) => this.root_.classList.remove(className), - setNativeControlDisabled: (disabled) => this.nativeControl_.disabled = disabled, - }); - } - - private initRipple_(): MDCRipple { - const foundation = new MDCRippleFoundation({ - ...MDCRipple.createAdapter(this), - deregisterInteractionHandler: (evtType: K, handler: SpecificEventListener) => - this.nativeControl_.removeEventListener(evtType, handler), - // Radio buttons technically go "active" whenever there is *any* keyboard interaction. This is not the - // UI we desire. - isSurfaceActive: () => false, - isUnbounded: () => true, - registerInteractionHandler: (evtType: K, handler: SpecificEventListener) => - this.nativeControl_.addEventListener(evtType, handler), - }); - return new MDCRipple(this.root_, foundation); - } - - /** - * Returns the state of the native control element, or null if the native control element is not present. - */ - private get nativeControl_(): HTMLInputElement { - const {NATIVE_CONTROL_SELECTOR} = MDCRadioFoundation.strings; - const el = this.root_.querySelector(NATIVE_CONTROL_SELECTOR); - if (!el) { - throw new Error(`Radio component requires a ${NATIVE_CONTROL_SELECTOR} element`); - } - return el; - } -} - -export {MDCRadio as default, MDCRadio}; export * from './adapter'; +export * from './component'; export * from './foundation'; diff --git a/packages/mdc-ripple/adapter.ts b/packages/mdc-ripple/adapter.ts index 68574dd3c3c..14316f87911 100644 --- a/packages/mdc-ripple/adapter.ts +++ b/packages/mdc-ripple/adapter.ts @@ -21,8 +21,8 @@ * THE SOFTWARE. */ -import {EventType, SpecificEventListener} from '@material/base/index'; -import {Point} from './types'; +import {EventType, SpecificEventListener} from '@material/base/types'; +import {MDCRipplePoint} from './types'; /** * Defines the shape of the adapter expected by the foundation. @@ -31,7 +31,7 @@ import {Point} from './types'; * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCRippleAdapter { +export interface MDCRippleAdapter { browserSupportsCssVars(): boolean; isUnbounded(): boolean; @@ -62,7 +62,5 @@ interface MDCRippleAdapter { computeBoundingRect(): ClientRect; - getWindowPageOffset(): Point; + getWindowPageOffset(): MDCRipplePoint; } - -export {MDCRippleAdapter as default, MDCRippleAdapter}; diff --git a/packages/mdc-ripple/component.ts b/packages/mdc-ripple/component.ts new file mode 100644 index 00000000000..3f2df2084ea --- /dev/null +++ b/packages/mdc-ripple/component.ts @@ -0,0 +1,114 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {ponyfill} from '@material/dom/index'; +import {MDCRippleAdapter} from './adapter'; +import {MDCRippleFoundation} from './foundation'; +import {MDCRippleAttachOpts, MDCRippleCapableSurface} from './types'; +import * as util from './util'; + +export type MDCRippleFactory = (el: Element, foundation?: MDCRippleFoundation) => MDCRipple; + +export class MDCRipple extends MDCComponent implements MDCRippleCapableSurface { + static attachTo(root: Element, opts: MDCRippleAttachOpts = {isUnbounded: undefined}): MDCRipple { + const ripple = new MDCRipple(root); + // Only override unbounded behavior if option is explicitly specified + if (opts.isUnbounded !== undefined) { + ripple.unbounded = opts.isUnbounded; + } + return ripple; + } + + static createAdapter(instance: MDCRippleCapableSurface): MDCRippleAdapter { + return { + addClass: (className) => instance.root_.classList.add(className), + browserSupportsCssVars: () => util.supportsCssVariables(window), + computeBoundingRect: () => instance.root_.getBoundingClientRect(), + containsEventTarget: (target) => instance.root_.contains(target as Node), + deregisterDocumentInteractionHandler: (evtType, handler) => + document.documentElement.removeEventListener(evtType, handler, util.applyPassive()), + deregisterInteractionHandler: (evtType, handler) => + instance.root_.removeEventListener(evtType, handler, util.applyPassive()), + deregisterResizeHandler: (handler) => window.removeEventListener('resize', handler), + getWindowPageOffset: () => ({x: window.pageXOffset, y: window.pageYOffset}), + isSurfaceActive: () => ponyfill.matches(instance.root_, ':active'), + isSurfaceDisabled: () => Boolean(instance.disabled), + isUnbounded: () => Boolean(instance.unbounded), + registerDocumentInteractionHandler: (evtType, handler) => + document.documentElement.addEventListener(evtType, handler, util.applyPassive()), + registerInteractionHandler: (evtType, handler) => + instance.root_.addEventListener(evtType, handler, util.applyPassive()), + registerResizeHandler: (handler) => window.addEventListener('resize', handler), + removeClass: (className) => instance.root_.classList.remove(className), + updateCssVariable: (varName, value) => (instance.root_ as HTMLElement).style.setProperty(varName, value), + }; + } + + // Public visibility for this property is required by MDCRippleCapableSurface. + root_!: Element; // assigned in MDCComponent constructor + + disabled = false; + + private unbounded_?: boolean; + + get unbounded(): boolean { + return Boolean(this.unbounded_); + } + + set unbounded(unbounded: boolean) { + this.unbounded_ = Boolean(unbounded); + this.setUnbounded_(); + } + + activate() { + this.foundation_.activate(); + } + + deactivate() { + this.foundation_.deactivate(); + } + + layout() { + this.foundation_.layout(); + } + + getDefaultFoundation() { + return new MDCRippleFoundation(MDCRipple.createAdapter(this)); + } + + initialSyncWithDOM() { + const root = this.root_ as HTMLElement; + this.unbounded = 'mdcRippleIsUnbounded' in root.dataset; + } + + /** + * Closure Compiler throws an access control error when directly accessing a + * protected or private property inside a getter/setter, like unbounded above. + * By accessing the protected property inside a method, we solve that problem. + * That's why this function exists. + */ + private setUnbounded_() { + this.foundation_.setUnbounded(Boolean(this.unbounded_)); + } +} diff --git a/packages/mdc-ripple/constants.ts b/packages/mdc-ripple/constants.ts index 41398e463a3..bf58a8f7409 100644 --- a/packages/mdc-ripple/constants.ts +++ b/packages/mdc-ripple/constants.ts @@ -21,7 +21,7 @@ * THE SOFTWARE. */ -const cssClasses = { +export const cssClasses = { // Ripple is a special case where the "root" component is really a "mixin" of sorts, // given that it's an 'upgrade' to an existing component. That being said it is the root // CSS class that all other CSS classes derive from. @@ -32,7 +32,7 @@ const cssClasses = { UNBOUNDED: 'mdc-ripple-upgraded--unbounded', }; -const strings = { +export const strings = { VAR_FG_SCALE: '--mdc-ripple-fg-scale', VAR_FG_SIZE: '--mdc-ripple-fg-size', VAR_FG_TRANSLATE_END: '--mdc-ripple-fg-translate-end', @@ -41,12 +41,10 @@ const strings = { VAR_TOP: '--mdc-ripple-top', }; -const numbers = { +export const numbers = { DEACTIVATION_TIMEOUT_MS: 225, // Corresponds to $mdc-ripple-translate-duration (i.e. activation animation duration) FG_DEACTIVATION_MS: 150, // Corresponds to $mdc-ripple-fade-out-duration (i.e. deactivation animation duration) INITIAL_ORIGIN_SCALE: 0.6, PADDING: 10, TAP_DELAY_MS: 300, // Delay between touch and simulated mouse events on touch devices }; - -export {cssClasses, strings, numbers}; diff --git a/packages/mdc-ripple/foundation.ts b/packages/mdc-ripple/foundation.ts index e855ee4010d..1eaaa353c94 100644 --- a/packages/mdc-ripple/foundation.ts +++ b/packages/mdc-ripple/foundation.ts @@ -24,7 +24,7 @@ import {MDCFoundation} from '@material/base/foundation'; import {MDCRippleAdapter} from './adapter'; import {cssClasses, numbers, strings} from './constants'; -import {Point} from './types'; +import {MDCRipplePoint} from './types'; import {getNormalizedEventCoords} from './util'; interface ActivationStateType { @@ -37,8 +37,8 @@ interface ActivationStateType { } interface FgTranslationCoordinates { - startPoint: Point; - endPoint: Point; + startPoint: MDCRipplePoint; + endPoint: MDCRipplePoint; } interface Coordinates { @@ -62,16 +62,16 @@ const POINTER_DEACTIVATION_EVENT_TYPES: DeactivationEventType[] = [ // simultaneous nested activations let activatedTargets: Array = []; -class MDCRippleFoundation extends MDCFoundation { - static get cssClasses(): {[key: string]: string} { +export class MDCRippleFoundation extends MDCFoundation { + static get cssClasses(): { [key: string]: string } { return cssClasses; } - static get strings(): {[key: string]: string} { + static get strings(): { [key: string]: string } { return strings; } - static get numbers(): {[key: string]: number} { + static get numbers(): { [key: string]: number } { return numbers; } @@ -208,12 +208,12 @@ class MDCRippleFoundation extends MDCFoundation { handleFocus(): void { requestAnimationFrame(() => - this.adapter_.addClass(MDCRippleFoundation.cssClasses.BG_FOCUSED)); + this.adapter_.addClass(MDCRippleFoundation.cssClasses.BG_FOCUSED)); } handleBlur(): void { requestAnimationFrame(() => - this.adapter_.removeClass(MDCRippleFoundation.cssClasses.BG_FOCUSED)); + this.adapter_.removeClass(MDCRippleFoundation.cssClasses.BG_FOCUSED)); } /** @@ -313,11 +313,11 @@ class MDCRippleFoundation extends MDCFoundation { activationState.isProgrammatic = evt === undefined; activationState.activationEvent = evt; activationState.wasActivatedByPointer = activationState.isProgrammatic ? false : evt !== undefined && ( - evt.type === 'mousedown' || evt.type === 'touchstart' || evt.type === 'pointerdown' + evt.type === 'mousedown' || evt.type === 'touchstart' || evt.type === 'pointerdown' ); const hasActivatedChild = evt !== undefined && activatedTargets.length > 0 && activatedTargets.some( - (target) => this.adapter_.containsEventTarget(target)); + (target) => this.adapter_.containsEventTarget(target)); if (hasActivatedChild) { // Immediately reset activation state, while preserving logic that prevents touch follow-on events this.resetActivationState_(); @@ -339,8 +339,8 @@ class MDCRippleFoundation extends MDCFoundation { activatedTargets = []; if (!activationState.wasElementMadeActive - && evt !== undefined - && ((evt as KeyboardEvent).key === ' ' || (evt as KeyboardEvent).keyCode === 32)) { + && evt !== undefined + && ((evt as KeyboardEvent).key === ' ' || (evt as KeyboardEvent).keyCode === 32)) { // If space was pressed, try again within an rAF call to detect :active, because different UAs report // active states inconsistently when they're called within event handling code: // - https://bugs.chromium.org/p/chromium/issues/detail?id=635971 @@ -400,9 +400,9 @@ class MDCRippleFoundation extends MDCFoundation { let startPoint; if (wasActivatedByPointer) { startPoint = getNormalizedEventCoords( - activationEvent, - this.adapter_.getWindowPageOffset(), - this.adapter_.computeBoundingRect(), + activationEvent, + this.adapter_.getWindowPageOffset(), + this.adapter_.computeBoundingRect(), ); } else { startPoint = { @@ -527,4 +527,4 @@ class MDCRippleFoundation extends MDCFoundation { } } -export {MDCRippleFoundation as default, MDCRippleFoundation}; +export default MDCRippleFoundation; diff --git a/packages/mdc-ripple/index.ts b/packages/mdc-ripple/index.ts index d0c51d010f8..3de836c29e1 100644 --- a/packages/mdc-ripple/index.ts +++ b/packages/mdc-ripple/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2016 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,97 +21,10 @@ * THE SOFTWARE. */ -import {MDCComponent} from '@material/base/component'; -import {ponyfill} from '@material/dom/index'; -import {MDCRippleAdapter} from './adapter'; -import {MDCRippleFoundation} from './foundation'; -import {RippleAttachOpts, RippleCapableSurface} from './types'; import * as util from './util'; -class MDCRipple extends MDCComponent implements RippleCapableSurface { - static attachTo(root: Element, opts: RippleAttachOpts = {isUnbounded: undefined}): MDCRipple { - const ripple = new MDCRipple(root); - // Only override unbounded behavior if option is explicitly specified - if (opts.isUnbounded !== undefined) { - ripple.unbounded = opts.isUnbounded; - } - return ripple; - } - - static createAdapter(instance: RippleCapableSurface): MDCRippleAdapter { - return { - addClass: (className) => instance.root_.classList.add(className), - browserSupportsCssVars: () => util.supportsCssVariables(window), - computeBoundingRect: () => instance.root_.getBoundingClientRect(), - containsEventTarget: (target) => instance.root_.contains(target as Node), - deregisterDocumentInteractionHandler: (evtType, handler) => - document.documentElement.removeEventListener(evtType, handler, util.applyPassive()), - deregisterInteractionHandler: (evtType, handler) => - instance.root_.removeEventListener(evtType, handler, util.applyPassive()), - deregisterResizeHandler: (handler) => window.removeEventListener('resize', handler), - getWindowPageOffset: () => ({x: window.pageXOffset, y: window.pageYOffset}), - isSurfaceActive: () => ponyfill.matches(instance.root_, ':active'), - isSurfaceDisabled: () => Boolean(instance.disabled), - isUnbounded: () => Boolean(instance.unbounded), - registerDocumentInteractionHandler: (evtType, handler) => - document.documentElement.addEventListener(evtType, handler, util.applyPassive()), - registerInteractionHandler: (evtType, handler) => - instance.root_.addEventListener(evtType, handler, util.applyPassive()), - registerResizeHandler: (handler) => window.addEventListener('resize', handler), - removeClass: (className) => instance.root_.classList.remove(className), - updateCssVariable: (varName, value) => (instance.root_ as HTMLElement).style.setProperty(varName, value), - }; - } - - // Public visibility for this property is required by RippleCapableSurface. - root_!: Element; // assigned in MDCComponent constructor - - disabled = false; - - private unbounded_?: boolean; - - get unbounded(): boolean { - return Boolean(this.unbounded_); - } - - set unbounded(unbounded: boolean) { - this.unbounded_ = Boolean(unbounded); - this.setUnbounded_(); - } - - activate() { - this.foundation_.activate(); - } - - deactivate() { - this.foundation_.deactivate(); - } - - layout() { - this.foundation_.layout(); - } - - getDefaultFoundation(): MDCRippleFoundation { - return new MDCRippleFoundation(MDCRipple.createAdapter(this)); - } - - initialSyncWithDOM() { - const root = this.root_ as HTMLElement; - this.unbounded = 'mdcRippleIsUnbounded' in root.dataset; - } - - /** - * Closure Compiler throws an access control error when directly accessing a - * protected or private property inside a getter/setter, like unbounded above. - * By accessing the protected property inside a method, we solve that problem. - * That's why this function exists. - */ - private setUnbounded_() { - this.foundation_.setUnbounded(Boolean(this.unbounded_)); - } -} - -export {MDCRipple as default, MDCRipple, util}; +export {util}; export * from './adapter'; +export * from './component'; export * from './foundation'; export * from './types'; diff --git a/packages/mdc-ripple/types.ts b/packages/mdc-ripple/types.ts index 95a85efc581..c5262f067a4 100644 --- a/packages/mdc-ripple/types.ts +++ b/packages/mdc-ripple/types.ts @@ -21,7 +21,7 @@ * THE SOFTWARE. */ -export interface Point { +export interface MDCRipplePoint { x: number; y: number; } @@ -29,7 +29,7 @@ export interface Point { /** * Options passed in when attaching a ripple to an object. */ -export interface RippleAttachOpts { +export interface MDCRippleAttachOpts { isUnbounded?: boolean; } @@ -39,7 +39,7 @@ export interface RippleAttachOpts { * unbounded Whether or not the ripple bleeds out of the bounds of the element. * disabled Whether or not the ripple is attached to a disabled component. */ -export interface RippleCapableSurface { +export interface MDCRippleCapableSurface { readonly root_: Element; unbounded?: boolean; disabled?: boolean; diff --git a/packages/mdc-ripple/util.ts b/packages/mdc-ripple/util.ts index 1dd65357d9c..97552da06bb 100644 --- a/packages/mdc-ripple/util.ts +++ b/packages/mdc-ripple/util.ts @@ -20,19 +20,19 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -import {Point} from './types'; +import {MDCRipplePoint} from './types'; /** * Stores result from supportsCssVariables to avoid redundant processing to * detect CSS custom variable support. */ -let supportsCssVariables_: boolean|undefined; +let supportsCssVariables_: boolean | undefined; /** * Stores result from applyPassive to avoid redundant processing to detect * passive event listener support. */ -let supportsPassive_: boolean|undefined; +let supportsPassive_: boolean | undefined; function detectEdgePseudoVarBug(windowObj: Window): boolean { // Detect versions of Edge with buggy var() support @@ -68,8 +68,8 @@ export function supportsCssVariables(windowObj: Window, forceRefresh = false): b // See: https://bugs.webkit.org/show_bug.cgi?id=154669 // See: README section on Safari const weAreFeatureDetectingSafari10plus = ( - CSS.supports('(--css-vars: yes)') && - CSS.supports('color', '#00000000') + CSS.supports('(--css-vars: yes)') && + CSS.supports('color', '#00000000') ); if (explicitlySupportsCssVars || weAreFeatureDetectingSafari10plus) { @@ -89,15 +89,18 @@ export function supportsCssVariables(windowObj: Window, forceRefresh = false): b * if so, use them. */ export function applyPassive(globalObj: Window = window, forceRefresh = false): - boolean|EventListenerOptions { + boolean | EventListenerOptions { if (supportsPassive_ === undefined || forceRefresh) { let isSupported = false; try { - globalObj.document.addEventListener('test', () => undefined, {get passive() { - isSupported = true; - return isSupported; - }}); - } catch (e) {} // tslint:disable-line:no-empty cannot throw error due to tests. tslint also disables console.log. + globalObj.document.addEventListener('test', () => undefined, { + get passive() { + isSupported = true; + return isSupported; + }, + }); + } catch (e) { + } // tslint:disable-line:no-empty cannot throw error due to tests. tslint also disables console.log. supportsPassive_ = isSupported; } @@ -105,7 +108,8 @@ export function applyPassive(globalObj: Window = window, forceRefresh = false): return supportsPassive_ ? {passive: true} as EventListenerOptions : false; } -export function getNormalizedEventCoords(evt: Event | undefined, pageOffset: Point, clientRect: ClientRect): Point { +export function getNormalizedEventCoords(evt: Event | undefined, pageOffset: MDCRipplePoint, clientRect: ClientRect): + MDCRipplePoint { if (!evt) { return {x: 0, y: 0}; } diff --git a/packages/mdc-select/adapter.ts b/packages/mdc-select/adapter.ts index a217c11fc1a..db2851063c3 100644 --- a/packages/mdc-select/adapter.ts +++ b/packages/mdc-select/adapter.ts @@ -28,7 +28,7 @@ * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCSelectAdapter { +export interface MDCSelectAdapter { /** * Adds class to root element. */ @@ -134,5 +134,3 @@ interface MDCSelectAdapter { */ setValid(isValid: boolean): void; } - -export {MDCSelectAdapter as default, MDCSelectAdapter}; diff --git a/packages/mdc-select/component.ts b/packages/mdc-select/component.ts new file mode 100644 index 00000000000..fcb3ca64287 --- /dev/null +++ b/packages/mdc-select/component.ts @@ -0,0 +1,595 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {CustomEventListener, SpecificEventListener} from '@material/base/types'; +import {MDCFloatingLabel, MDCFloatingLabelFactory} from '@material/floating-label/index'; +import {MDCLineRipple, MDCLineRippleFactory} from '@material/line-ripple/index'; +import * as menuSurfaceConstants from '@material/menu-surface/constants'; +import * as menuConstants from '@material/menu/constants'; +import {MDCMenu, MDCMenuFactory, MDCMenuItemEvent} from '@material/menu/index'; +import {MDCNotchedOutline, MDCNotchedOutlineFactory} from '@material/notched-outline/index'; +import {MDCRipple, MDCRippleAdapter, MDCRippleCapableSurface, MDCRippleFoundation} from '@material/ripple/index'; +import {MDCSelectAdapter} from './adapter'; +import {cssClasses, strings} from './constants'; +import {MDCSelectFoundation} from './foundation'; +import {MDCSelectHelperText, MDCSelectHelperTextFactory} from './helper-text/index'; +import {MDCSelectIcon, MDCSelectIconFactory} from './icon/index'; +import {MDCSelectEventDetail, MDCSelectFoundationMap} from './types'; + +type PointerEventType = 'mousedown' | 'touchstart'; + +const POINTER_EVENTS: PointerEventType[] = ['mousedown', 'touchstart']; +const VALIDATION_ATTR_WHITELIST = ['required', 'aria-required']; + +export class MDCSelect extends MDCComponent implements MDCRippleCapableSurface { + static attachTo(root: Element): MDCSelect { + return new MDCSelect(root); + } + + // Public visibility for this property is required by MDCRippleCapableSurface. + root_!: HTMLElement; // assigned in MDCComponent constructor + + private menu_!: MDCMenu | null; // assigned in enhancedSelectSetup_() + private menuOpened_ = false; + + // Exactly one of these fields must be non-null. + private nativeControl_!: HTMLSelectElement | null; // assigned in initialize() + private selectedText_!: HTMLElement | null; // assigned in initialize() + + private targetElement_!: HTMLElement; // assigned in initialize() + + private hiddenInput_!: HTMLInputElement | null; + private leadingIcon_?: MDCSelectIcon; + private helperText_!: MDCSelectHelperText | null; + private menuElement_!: Element | null; + private ripple!: MDCRipple | null; + private lineRipple_!: MDCLineRipple | null; + private label_!: MDCFloatingLabel | null; + private outline_!: MDCNotchedOutline | null; + private handleChange_!: SpecificEventListener<'change'>; // assigned in initialize() + private handleFocus_!: SpecificEventListener<'focus'>; // assigned in initialize() + private handleBlur_!: SpecificEventListener<'blur'>; // assigned in initialize() + private handleClick_!: SpecificEventListener; // assigned in initialize() + private handleKeydown_!: SpecificEventListener<'keydown'>; // assigned in initialize() + private handleMenuOpened_!: EventListener; // assigned in initialize() + private handleMenuClosed_!: EventListener; // assigned in initialize() + private handleMenuSelected_!: CustomEventListener; // assigned in initialize() + private validationObserver_!: MutationObserver; // assigned in initialize() + + initialize( + labelFactory: MDCFloatingLabelFactory = (el) => new MDCFloatingLabel(el), + lineRippleFactory: MDCLineRippleFactory = (el) => new MDCLineRipple(el), + outlineFactory: MDCNotchedOutlineFactory = (el) => new MDCNotchedOutline(el), + menuFactory: MDCMenuFactory = (el) => new MDCMenu(el), + iconFactory: MDCSelectIconFactory = (el) => new MDCSelectIcon(el), + helperTextFactory: MDCSelectHelperTextFactory = (el) => new MDCSelectHelperText(el), + ) { + this.nativeControl_ = this.root_.querySelector(strings.NATIVE_CONTROL_SELECTOR); + this.selectedText_ = this.root_.querySelector(strings.SELECTED_TEXT_SELECTOR); + + const targetElement = this.nativeControl_ || this.selectedText_; + if (!targetElement) { + throw new Error( + 'MDCSelect: Missing required element: Exactly one of the following selectors must be present: ' + + `'${strings.NATIVE_CONTROL_SELECTOR}' or '${strings.SELECTED_TEXT_SELECTOR}'`, + ); + } + + this.targetElement_ = targetElement; + if (this.targetElement_.hasAttribute(strings.ARIA_CONTROLS)) { + const helperTextElement = document.getElementById(this.targetElement_.getAttribute(strings.ARIA_CONTROLS)!); + if (helperTextElement) { + this.helperText_ = helperTextFactory(helperTextElement); + } + } + + if (this.selectedText_) { + this.enhancedSelectSetup_(menuFactory); + } + + const labelElement = this.root_.querySelector(strings.LABEL_SELECTOR); + this.label_ = labelElement ? labelFactory(labelElement) : null; + + const lineRippleElement = this.root_.querySelector(strings.LINE_RIPPLE_SELECTOR); + this.lineRipple_ = lineRippleElement ? lineRippleFactory(lineRippleElement) : null; + + const outlineElement = this.root_.querySelector(strings.OUTLINE_SELECTOR); + this.outline_ = outlineElement ? outlineFactory(outlineElement) : null; + + const leadingIcon = this.root_.querySelector(strings.LEADING_ICON_SELECTOR); + if (leadingIcon) { + this.root_.classList.add(cssClasses.WITH_LEADING_ICON); + this.leadingIcon_ = iconFactory(leadingIcon); + + if (this.menuElement_) { + this.menuElement_.classList.add(cssClasses.WITH_LEADING_ICON); + } + } + + if (!this.root_.classList.contains(cssClasses.OUTLINED)) { + this.ripple = this.createRipple_(); + } + + // The required state needs to be sync'd before the mutation observer is added. + this.initialSyncRequiredState_(); + this.addMutationObserverForRequired_(); + } + + /** + * Initializes the select's event listeners and internal state based + * on the environment's state. + */ + initialSyncWithDOM() { + this.handleChange_ = () => this.foundation_.handleChange(/* didChange */ true); + this.handleFocus_ = () => this.foundation_.handleFocus(); + this.handleBlur_ = () => this.foundation_.handleBlur(); + this.handleClick_ = (evt) => { + if (this.selectedText_) { + this.selectedText_.focus(); + } + this.foundation_.handleClick(this.getNormalizedXCoordinate_(evt)); + }; + this.handleKeydown_ = (evt) => this.foundation_.handleKeydown(evt); + this.handleMenuSelected_ = (evtData) => this.selectedIndex = evtData.detail.index; + this.handleMenuOpened_ = () => { + // Menu should open to the last selected element. + if (this.selectedIndex >= 0) { + const selectedItemEl = this.menu_!.items[this.selectedIndex] as HTMLElement; + selectedItemEl.focus(); + } + }; + this.handleMenuClosed_ = () => { + // menuOpened_ is used to track the state of the menu opening or closing since the menu.open function + // will return false if the menu is still closing and this method listens to the closed event which + // occurs after the menu is already closed. + this.menuOpened_ = false; + this.selectedText_!.removeAttribute('aria-expanded'); + if (document.activeElement !== this.selectedText_) { + this.foundation_.handleBlur(); + } + }; + + this.targetElement_.addEventListener('change', this.handleChange_); + this.targetElement_.addEventListener('focus', this.handleFocus_); + this.targetElement_.addEventListener('blur', this.handleBlur_); + + POINTER_EVENTS.forEach((evtType) => { + this.targetElement_.addEventListener(evtType, this.handleClick_ as EventListener); + }); + + if (this.menuElement_) { + this.selectedText_!.addEventListener('keydown', this.handleKeydown_); + this.menu_!.listen(menuSurfaceConstants.strings.CLOSED_EVENT, this.handleMenuClosed_); + this.menu_!.listen(menuSurfaceConstants.strings.OPENED_EVENT, this.handleMenuOpened_); + this.menu_!.listen(menuConstants.strings.SELECTED_EVENT, this.handleMenuSelected_); + + if (this.hiddenInput_ && this.hiddenInput_.value) { + // If the hidden input already has a value, use it to restore the select's value. + // This can happen e.g. if the user goes back or (in some browsers) refreshes the page. + const enhancedAdapterMethods = this.getEnhancedSelectAdapterMethods_(); + enhancedAdapterMethods.setValue(this.hiddenInput_.value); + } else if (this.menuElement_.querySelector(strings.SELECTED_ITEM_SELECTOR)) { + // If an element is selected, the select should set the initial selected text. + const enhancedAdapterMethods = this.getEnhancedSelectAdapterMethods_(); + enhancedAdapterMethods.setValue(enhancedAdapterMethods.getValue()); + } + } + + // Initially sync floating label + this.foundation_.handleChange(/* didChange */ false); + + if (this.root_.classList.contains(cssClasses.DISABLED) + || (this.nativeControl_ && this.nativeControl_.disabled)) { + this.disabled = true; + } + } + + destroy() { + this.targetElement_.removeEventListener('change', this.handleChange_); + this.targetElement_.removeEventListener('focus', this.handleFocus_); + this.targetElement_.removeEventListener('blur', this.handleBlur_); + this.targetElement_.removeEventListener('keydown', this.handleKeydown_); + POINTER_EVENTS.forEach((evtType) => { + this.targetElement_.removeEventListener(evtType, this.handleClick_ as EventListener); + }); + + if (this.menu_) { + this.menu_.unlisten(menuSurfaceConstants.strings.CLOSED_EVENT, this.handleMenuClosed_); + this.menu_.unlisten(menuSurfaceConstants.strings.OPENED_EVENT, this.handleMenuOpened_); + this.menu_.unlisten(menuConstants.strings.SELECTED_EVENT, this.handleMenuSelected_); + this.menu_.destroy(); + } + + if (this.ripple) { + this.ripple.destroy(); + } + if (this.outline_) { + this.outline_.destroy(); + } + if (this.leadingIcon_) { + this.leadingIcon_.destroy(); + } + if (this.helperText_) { + this.helperText_.destroy(); + } + if (this.validationObserver_) { + this.validationObserver_.disconnect(); + } + + super.destroy(); + } + + get value(): string { + return this.foundation_.getValue(); + } + + set value(value: string) { + this.foundation_.setValue(value); + } + + get selectedIndex(): number { + let selectedIndex = -1; + if (this.menuElement_ && this.menu_) { + const selectedEl = this.menuElement_.querySelector(strings.SELECTED_ITEM_SELECTOR)!; + selectedIndex = this.menu_.items.indexOf(selectedEl); + } else if (this.nativeControl_) { + selectedIndex = this.nativeControl_.selectedIndex; + } + return selectedIndex; + } + + set selectedIndex(selectedIndex: number) { + this.foundation_.setSelectedIndex(selectedIndex); + } + + get disabled(): boolean { + return this.root_.classList.contains(cssClasses.DISABLED) || + (this.nativeControl_ ? this.nativeControl_.disabled : false); + } + + set disabled(disabled: boolean) { + this.foundation_.setDisabled(disabled); + } + + set leadingIconAriaLabel(label: string) { + this.foundation_.setLeadingIconAriaLabel(label); + } + + /** + * Sets the text content of the leading icon. + */ + set leadingIconContent(content: string) { + this.foundation_.setLeadingIconContent(content); + } + + /** + * Sets the text content of the helper text. + */ + set helperTextContent(content: string) { + this.foundation_.setHelperTextContent(content); + } + + /** + * Sets the current invalid state of the select. + */ + set valid(isValid: boolean) { + this.foundation_.setValid(isValid); + } + + /** + * Checks if the select is in a valid state. + */ + get valid(): boolean { + return this.foundation_.isValid(); + } + + /** + * Sets the control to the required state. + */ + set required(isRequired: boolean) { + if (this.nativeControl_) { + this.nativeControl_.required = isRequired; + } else { + if (isRequired) { + this.selectedText_!.setAttribute('aria-required', isRequired.toString()); + } else { + this.selectedText_!.removeAttribute('aria-required'); + } + } + } + + /** + * Returns whether the select is required. + */ + get required(): boolean { + if (this.nativeControl_) { + return this.nativeControl_.required; + } else { + return this.selectedText_!.getAttribute('aria-required') === 'true'; + } + } + + /** + * Recomputes the outline SVG path for the outline element. + */ + layout() { + this.foundation_.layout(); + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + const adapter: MDCSelectAdapter = { + ...(this.nativeControl_ ? this.getNativeSelectAdapterMethods_() : this.getEnhancedSelectAdapterMethods_()), + ...this.getCommonAdapterMethods_(), + ...this.getOutlineAdapterMethods_(), + ...this.getLabelAdapterMethods_(), + }; + return new MDCSelectFoundation(adapter, this.getFoundationMap_()); + } + + /** + * Handles setup for the enhanced menu. + */ + private enhancedSelectSetup_(menuFactory: MDCMenuFactory) { + const isDisabled = this.root_.classList.contains(cssClasses.DISABLED); + this.selectedText_!.setAttribute('tabindex', isDisabled ? '-1' : '0'); + this.hiddenInput_ = this.root_.querySelector(strings.HIDDEN_INPUT_SELECTOR); + this.menuElement_ = this.root_.querySelector(strings.MENU_SELECTOR)!; + this.menu_ = menuFactory(this.menuElement_); + this.menu_.hoistMenuToBody(); + this.menu_.setAnchorElement(this.root_); + this.menu_.setAnchorCorner(menuSurfaceConstants.Corner.BOTTOM_START); + this.menu_.wrapFocus = false; + } + + private createRipple_(): MDCRipple { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + // tslint:disable:object-literal-sort-keys + const adapter: MDCRippleAdapter = { + ...MDCRipple.createAdapter(this), + registerInteractionHandler: (evtType, handler) => this.targetElement_.addEventListener(evtType, handler), + deregisterInteractionHandler: (evtType, handler) => this.targetElement_.removeEventListener(evtType, handler), + }; + // tslint:enable:object-literal-sort-keys + return new MDCRipple(this.root_, new MDCRippleFoundation(adapter)); + } + + private getNativeSelectAdapterMethods_() { + // tslint:disable:object-literal-sort-keys + return { + getValue: () => this.nativeControl_!.value, + setValue: (value: string) => { + this.nativeControl_!.value = value; + }, + openMenu: () => undefined, + closeMenu: () => undefined, + isMenuOpen: () => false, + setSelectedIndex: (index: number) => { + this.nativeControl_!.selectedIndex = index; + }, + setDisabled: (isDisabled: boolean) => { + this.nativeControl_!.disabled = isDisabled; + }, + setValid: (isValid: boolean) => { + if (isValid) { + this.root_.classList.remove(cssClasses.INVALID); + } else { + this.root_.classList.add(cssClasses.INVALID); + } + }, + checkValidity: () => this.nativeControl_!.checkValidity(), + }; + // tslint:enable:object-literal-sort-keys + } + + private getEnhancedSelectAdapterMethods_() { + // tslint:disable:object-literal-sort-keys + return { + getValue: () => { + const listItem = this.menuElement_!.querySelector(strings.SELECTED_ITEM_SELECTOR); + if (listItem && listItem.hasAttribute(strings.ENHANCED_VALUE_ATTR)) { + return listItem.getAttribute(strings.ENHANCED_VALUE_ATTR) || ''; + } + return ''; + }, + setValue: (value: string) => { + const element = this.menuElement_!.querySelector(`[${strings.ENHANCED_VALUE_ATTR}="${value}"]`); + this.setEnhancedSelectedIndex_(element ? this.menu_!.items.indexOf(element) : -1); + }, + openMenu: () => { + if (this.menu_ && !this.menu_.open) { + this.menu_.open = true; + this.menuOpened_ = true; + this.selectedText_!.setAttribute('aria-expanded', 'true'); + } + }, + closeMenu: () => { + if (this.menu_ && this.menu_.open) { + this.menu_.open = false; + } + }, + isMenuOpen: () => Boolean(this.menu_) && this.menuOpened_, + setSelectedIndex: (index: number) => this.setEnhancedSelectedIndex_(index), + setDisabled: (isDisabled: boolean) => { + this.selectedText_!.setAttribute('tabindex', isDisabled ? '-1' : '0'); + this.selectedText_!.setAttribute('aria-disabled', isDisabled.toString()); + if (this.hiddenInput_) { + this.hiddenInput_.disabled = isDisabled; + } + }, + checkValidity: () => { + const classList = this.root_.classList; + if (classList.contains(cssClasses.REQUIRED) && !classList.contains(cssClasses.DISABLED)) { + // See notes for required attribute under https://www.w3.org/TR/html52/sec-forms.html#the-select-element + // TL;DR: Invalid if no index is selected, or if the first index is selected and has an empty value. + return this.selectedIndex !== -1 && (this.selectedIndex !== 0 || Boolean(this.value)); + } else { + return true; + } + }, + setValid: (isValid: boolean) => { + this.selectedText_!.setAttribute('aria-invalid', (!isValid).toString()); + if (isValid) { + this.root_.classList.remove(cssClasses.INVALID); + } else { + this.root_.classList.add(cssClasses.INVALID); + } + }, + }; + // tslint:enable:object-literal-sort-keys + } + + private getCommonAdapterMethods_() { + // tslint:disable:object-literal-sort-keys + return { + addClass: (className: string) => this.root_.classList.add(className), + removeClass: (className: string) => this.root_.classList.remove(className), + hasClass: (className: string) => this.root_.classList.contains(className), + setRippleCenter: (normalizedX: number) => this.lineRipple_ && this.lineRipple_.setRippleCenter(normalizedX), + activateBottomLine: () => this.lineRipple_ && this.lineRipple_.activate(), + deactivateBottomLine: () => this.lineRipple_ && this.lineRipple_.deactivate(), + notifyChange: (value: string) => { + const index = this.selectedIndex; + this.emit(strings.CHANGE_EVENT, {value, index}, true /* shouldBubble */); + }, + }; + // tslint:enable:object-literal-sort-keys + } + + private getOutlineAdapterMethods_() { + // tslint:disable:object-literal-sort-keys + return { + hasOutline: () => Boolean(this.outline_), + notchOutline: (labelWidth: number) => this.outline_ && this.outline_.notch(labelWidth), + closeOutline: () => this.outline_ && this.outline_.closeNotch(), + }; + // tslint:enable:object-literal-sort-keys + } + + private getLabelAdapterMethods_() { + return { + floatLabel: (shouldFloat: boolean) => this.label_ && this.label_.float(shouldFloat), + getLabelWidth: () => this.label_ ? this.label_.getWidth() : 0, + }; + } + + /** + * Calculates where the line ripple should start based on the x coordinate within the component. + */ + private getNormalizedXCoordinate_(evt: MouseEvent | TouchEvent): number { + const targetClientRect = (evt.target as Element).getBoundingClientRect(); + const xCoordinate = this.isTouchEvent_(evt) ? evt.touches[0].clientX : evt.clientX; + return xCoordinate - targetClientRect.left; + } + + private isTouchEvent_(evt: MouseEvent | TouchEvent): evt is TouchEvent { + return Boolean((evt as TouchEvent).touches); + } + + /** + * Returns a map of all subcomponents to subfoundations. + */ + private getFoundationMap_(): Partial { + return { + helperText: this.helperText_ ? this.helperText_.foundation : undefined, + leadingIcon: this.leadingIcon_ ? this.leadingIcon_.foundation : undefined, + }; + } + + private setEnhancedSelectedIndex_(index: number) { + const selectedItem = this.menu_!.items[index]; + this.selectedText_!.textContent = selectedItem ? selectedItem.textContent!.trim() : ''; + const previouslySelected = this.menuElement_!.querySelector(strings.SELECTED_ITEM_SELECTOR); + + if (previouslySelected) { + previouslySelected.classList.remove(cssClasses.SELECTED_ITEM_CLASS); + previouslySelected.removeAttribute(strings.ARIA_SELECTED_ATTR); + } + + if (selectedItem) { + selectedItem.classList.add(cssClasses.SELECTED_ITEM_CLASS); + selectedItem.setAttribute(strings.ARIA_SELECTED_ATTR, 'true'); + } + + // Synchronize hidden input's value with data-value attribute of selected item. + // This code path is also followed when setting value directly, so this covers all cases. + if (this.hiddenInput_) { + this.hiddenInput_.value = selectedItem ? selectedItem.getAttribute(strings.ENHANCED_VALUE_ATTR) || '' : ''; + } + + this.layout(); + } + + private initialSyncRequiredState_() { + const isRequired = + (this.targetElement_ as HTMLSelectElement).required + || this.targetElement_.getAttribute('aria-required') === 'true' + || this.root_.classList.contains(cssClasses.REQUIRED); + if (isRequired) { + if (this.nativeControl_) { + this.nativeControl_.required = true; + } else { + this.selectedText_!.setAttribute('aria-required', 'true'); + } + this.root_.classList.add(cssClasses.REQUIRED); + } + } + + private addMutationObserverForRequired_() { + const observerHandler = (attributesList: string[]) => { + attributesList.some((attributeName) => { + if (VALIDATION_ATTR_WHITELIST.indexOf(attributeName) === -1) { + return false; + } + + if (this.selectedText_) { + if (this.selectedText_.getAttribute('aria-required') === 'true') { + this.root_.classList.add(cssClasses.REQUIRED); + } else { + this.root_.classList.remove(cssClasses.REQUIRED); + } + } else { + if (this.nativeControl_!.required) { + this.root_.classList.add(cssClasses.REQUIRED); + } else { + this.root_.classList.remove(cssClasses.REQUIRED); + } + } + + return true; + }); + }; + + const getAttributesList = (mutationsList: MutationRecord[]): string[] => { + return mutationsList + .map((mutation) => mutation.attributeName) + .filter((attributeName) => attributeName) as string[]; + }; + const observer = new MutationObserver((mutationsList) => observerHandler(getAttributesList(mutationsList))); + observer.observe(this.targetElement_, {attributes: true}); + this.validationObserver_ = observer; + } +} diff --git a/packages/mdc-select/foundation.ts b/packages/mdc-select/foundation.ts index 0722968ddcb..886eda0d826 100644 --- a/packages/mdc-select/foundation.ts +++ b/packages/mdc-select/foundation.ts @@ -26,9 +26,9 @@ import {MDCSelectAdapter} from './adapter'; import {cssClasses, numbers, strings} from './constants'; import {MDCSelectHelperTextFoundation} from './helper-text/foundation'; import {MDCSelectIconFoundation} from './icon/foundation'; -import {FoundationMapType} from './types'; +import {MDCSelectFoundationMap} from './types'; -class MDCSelectFoundation extends MDCFoundation { +export class MDCSelectFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } @@ -80,7 +80,7 @@ class MDCSelectFoundation extends MDCFoundation { * @param adapter * @param foundationMap Map from subcomponent names to their subfoundations. */ - constructor(adapter?: Partial, foundationMap: Partial = {}) { + constructor(adapter?: Partial, foundationMap: Partial = {}) { super({...MDCSelectFoundation.defaultAdapter, ...adapter}); this.leadingIcon_ = foundationMap.leadingIcon; @@ -262,4 +262,4 @@ class MDCSelectFoundation extends MDCFoundation { } } -export {MDCSelectFoundation as default, MDCSelectFoundation}; +export default MDCSelectFoundation; diff --git a/packages/mdc-select/helper-text/adapter.ts b/packages/mdc-select/helper-text/adapter.ts index e62a94cece1..06534cb81b5 100644 --- a/packages/mdc-select/helper-text/adapter.ts +++ b/packages/mdc-select/helper-text/adapter.ts @@ -28,7 +28,7 @@ * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCSelectHelperTextAdapter { +export interface MDCSelectHelperTextAdapter { /** * Adds a class to the helper text element. */ @@ -59,5 +59,3 @@ interface MDCSelectHelperTextAdapter { */ setContent(content: string): void; } - -export {MDCSelectHelperTextAdapter as default, MDCSelectHelperTextAdapter}; diff --git a/packages/mdc-select/helper-text/component.ts b/packages/mdc-select/helper-text/component.ts new file mode 100644 index 00000000000..5314f3584be --- /dev/null +++ b/packages/mdc-select/helper-text/component.ts @@ -0,0 +1,57 @@ +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {MDCSelectHelperTextAdapter} from './adapter'; +import {MDCSelectHelperTextFoundation} from './foundation'; + +export type MDCSelectHelperTextFactory = + (el: Element, foundation?: MDCSelectHelperTextFoundation) => MDCSelectHelperText; + +export class MDCSelectHelperText extends MDCComponent { + static attachTo(root: Element): MDCSelectHelperText { + return new MDCSelectHelperText(root); + } + + get foundation(): MDCSelectHelperTextFoundation { + return this.foundation_; + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + // tslint:disable:object-literal-sort-keys + const adapter: MDCSelectHelperTextAdapter = { + addClass: (className) => this.root_.classList.add(className), + removeClass: (className) => this.root_.classList.remove(className), + hasClass: (className) => this.root_.classList.contains(className), + setAttr: (attr, value) => this.root_.setAttribute(attr, value), + removeAttr: (attr) => this.root_.removeAttribute(attr), + setContent: (content) => { + this.root_.textContent = content; + }, + }; + // tslint:enable:object-literal-sort-keys + return new MDCSelectHelperTextFoundation(adapter); + } +} diff --git a/packages/mdc-select/helper-text/foundation.ts b/packages/mdc-select/helper-text/foundation.ts index bb7ee871642..4d9a2e34313 100644 --- a/packages/mdc-select/helper-text/foundation.ts +++ b/packages/mdc-select/helper-text/foundation.ts @@ -25,7 +25,7 @@ import {MDCFoundation} from '@material/base/foundation'; import {MDCSelectHelperTextAdapter} from './adapter'; import {cssClasses, strings} from './constants'; -class MDCSelectHelperTextFoundation extends MDCFoundation { +export class MDCSelectHelperTextFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } @@ -52,7 +52,7 @@ class MDCSelectHelperTextFoundation extends MDCFoundation) { super({...MDCSelectHelperTextFoundation.defaultAdapter, ...adapter}); -} + } /** * Sets the content of the helper text field. @@ -117,4 +117,4 @@ class MDCSelectHelperTextFoundation extends MDCFoundation { - static attachTo(root: Element): MDCSelectHelperText { - return new MDCSelectHelperText(root); - } - - get foundation(): MDCSelectHelperTextFoundation { - return this.foundation_; - } - - getDefaultFoundation(): MDCSelectHelperTextFoundation { - // tslint:disable:object-literal-sort-keys - return new MDCSelectHelperTextFoundation({ - addClass: (className) => this.root_.classList.add(className), - removeClass: (className) => this.root_.classList.remove(className), - hasClass: (className) => this.root_.classList.contains(className), - setAttr: (attr, value) => this.root_.setAttribute(attr, value), - removeAttr: (attr) => this.root_.removeAttribute(attr), - setContent: (content) => { this.root_.textContent = content; }, - }); - // tslint:enable:object-literal-sort-keys - } -} - -export {MDCSelectHelperText as default, MDCSelectHelperText}; export * from './adapter'; +export * from './component'; export * from './foundation'; diff --git a/packages/mdc-select/icon/adapter.ts b/packages/mdc-select/icon/adapter.ts index f9cf4d5325f..ba3e8374c5a 100644 --- a/packages/mdc-select/icon/adapter.ts +++ b/packages/mdc-select/icon/adapter.ts @@ -21,7 +21,7 @@ * THE SOFTWARE. */ -import {EventType, SpecificEventListener} from '@material/base/index'; +import {EventType, SpecificEventListener} from '@material/base/types'; /** * Defines the shape of the adapter expected by the foundation. @@ -30,7 +30,7 @@ import {EventType, SpecificEventListener} from '@material/base/index'; * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCSelectIconAdapter { +export interface MDCSelectIconAdapter { /** * Gets the value of an attribute on the icon element. */ @@ -54,17 +54,15 @@ interface MDCSelectIconAdapter { /** * Registers an event listener on the icon element for a given event. */ - registerInteractionHandler(evtType: E, handler: SpecificEventListener): void; + registerInteractionHandler(evtType: K, handler: SpecificEventListener): void; /** * Deregisters an event listener on the icon element for a given event. */ - deregisterInteractionHandler(evtType: E, handler: SpecificEventListener): void; + deregisterInteractionHandler(evtType: K, handler: SpecificEventListener): void; /** * Emits a custom event "MDCSelect:icon" denoting a user has clicked the icon. */ notifyIconAction(): void; } - -export {MDCSelectIconAdapter as default, MDCSelectIconAdapter}; diff --git a/packages/mdc-select/icon/component.ts b/packages/mdc-select/icon/component.ts new file mode 100644 index 00000000000..82cdaae24bc --- /dev/null +++ b/packages/mdc-select/icon/component.ts @@ -0,0 +1,58 @@ +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {MDCSelectIconAdapter} from './adapter'; +import {MDCSelectIconFoundation} from './foundation'; + +export type MDCSelectIconFactory = (el: Element, foundation?: MDCSelectIconFoundation) => MDCSelectIcon; + +export class MDCSelectIcon extends MDCComponent { + static attachTo(root: Element): MDCSelectIcon { + return new MDCSelectIcon(root); + } + + get foundation(): MDCSelectIconFoundation { + return this.foundation_; + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + // tslint:disable:object-literal-sort-keys + const adapter: MDCSelectIconAdapter = { + getAttr: (attr) => this.root_.getAttribute(attr), + setAttr: (attr, value) => this.root_.setAttribute(attr, value), + removeAttr: (attr) => this.root_.removeAttribute(attr), + setContent: (content) => { + this.root_.textContent = content; + }, + registerInteractionHandler: (evtType, handler) => this.listen(evtType, handler), + deregisterInteractionHandler: (evtType, handler) => this.unlisten(evtType, handler), + notifyIconAction: () => this.emit( + MDCSelectIconFoundation.strings.ICON_EVENT, {} /* evtData */, true /* shouldBubble */), + }; + // tslint:enable:object-literal-sort-keys + return new MDCSelectIconFoundation(adapter); + } +} diff --git a/packages/mdc-select/icon/foundation.ts b/packages/mdc-select/icon/foundation.ts index 69a4613b238..ad246fdca62 100644 --- a/packages/mdc-select/icon/foundation.ts +++ b/packages/mdc-select/icon/foundation.ts @@ -30,7 +30,7 @@ type InteractionEventType = 'click' | 'keydown'; const INTERACTION_EVENTS: InteractionEventType[] = ['click', 'keydown']; -class MDCSelectIconFoundation extends MDCFoundation { +export class MDCSelectIconFoundation extends MDCFoundation { static get strings() { return strings; } @@ -107,4 +107,4 @@ class MDCSelectIconFoundation extends MDCFoundation { } } -export {MDCSelectIconFoundation as default, MDCSelectIconFoundation}; +export default MDCSelectIconFoundation; diff --git a/packages/mdc-select/icon/index.ts b/packages/mdc-select/icon/index.ts index 58c27f72220..f8c89ac94f3 100644 --- a/packages/mdc-select/icon/index.ts +++ b/packages/mdc-select/icon/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2018 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,34 +21,6 @@ * THE SOFTWARE. */ -import {MDCComponent} from '@material/base/component'; -import {MDCSelectIconFoundation} from './foundation'; - -class MDCSelectIcon extends MDCComponent { - static attachTo(root: Element): MDCSelectIcon { - return new MDCSelectIcon(root); - } - - get foundation(): MDCSelectIconFoundation { - return this.foundation_; - } - - getDefaultFoundation(): MDCSelectIconFoundation { - // tslint:disable:object-literal-sort-keys - return new MDCSelectIconFoundation({ - getAttr: (attr) => this.root_.getAttribute(attr), - setAttr: (attr, value) => this.root_.setAttribute(attr, value), - removeAttr: (attr) => this.root_.removeAttribute(attr), - setContent: (content) => { this.root_.textContent = content; }, - registerInteractionHandler: (evtType, handler) => this.root_.addEventListener(evtType, handler), - deregisterInteractionHandler: (evtType, handler) => this.root_.removeEventListener(evtType, handler), - notifyIconAction: () => this.emit( - MDCSelectIconFoundation.strings.ICON_EVENT, {} /* evtData */, true /* shouldBubble */), - }); - // tslint:enable:object-literal-sort-keys - } -} - -export {MDCSelectIcon as default, MDCSelectIcon}; export * from './adapter'; +export * from './component'; export * from './foundation'; diff --git a/packages/mdc-select/index.ts b/packages/mdc-select/index.ts index a69222aab70..5fdc3c77a4e 100644 --- a/packages/mdc-select/index.ts +++ b/packages/mdc-select/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2016 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,583 +21,9 @@ * THE SOFTWARE. */ -import {CustomEventListener, MDCComponent} from '@material/base/index'; -import {EventType, SpecificEventListener} from '@material/base/index'; -import {MDCFloatingLabel} from '@material/floating-label/index'; -import {MDCLineRipple} from '@material/line-ripple/index'; -import * as menuSurfaceConstants from '@material/menu-surface/constants'; -import * as menuConstants from '@material/menu/constants'; -import {MDCMenu, MenuItemEvent} from '@material/menu/index'; -import {MDCNotchedOutline} from '@material/notched-outline/index'; -import {MDCRipple, MDCRippleFoundation, RippleCapableSurface} from '@material/ripple/index'; -import {cssClasses, strings} from './constants'; -import {MDCSelectFoundation} from './foundation'; -import {MDCSelectHelperText} from './helper-text'; -import {MDCSelectIcon} from './icon'; -import { - FoundationMapType, - HelperTextFactory, IconFactory, - LabelFactory, - LineRippleFactory, MenuFactory, - OutlineFactory, SelectEventDetail, -} from './types'; - -type PointerEventType = 'mousedown' | 'touchstart'; - -const POINTER_EVENTS: PointerEventType[] = ['mousedown', 'touchstart']; -const VALIDATION_ATTR_WHITELIST = ['required', 'aria-required']; - -class MDCSelect extends MDCComponent implements RippleCapableSurface { - static attachTo(root: Element): MDCSelect { - return new MDCSelect(root); - } - - // Public visibility for this property is required by RippleCapableSurface. - root_!: HTMLElement; // assigned in MDCComponent constructor - - private menu_!: MDCMenu | null; // assigned in enhancedSelectSetup_() - private menuOpened_ = false; - - // Exactly one of these fields must be non-null. - private nativeControl_!: HTMLSelectElement | null; // assigned in initialize() - private selectedText_!: HTMLElement | null; // assigned in initialize() - - private targetElement_!: HTMLElement; // assigned in initialize() - - private hiddenInput_!: HTMLInputElement | null; - private leadingIcon_?: MDCSelectIcon; - private helperText_!: MDCSelectHelperText | null; - private menuElement_!: Element | null; - private ripple!: MDCRipple | null; - private lineRipple_!: MDCLineRipple | null; - private label_!: MDCFloatingLabel | null; - private outline_!: MDCNotchedOutline | null; - private handleChange_!: SpecificEventListener<'change'>; // assigned in initialize() - private handleFocus_!: SpecificEventListener<'focus'>; // assigned in initialize() - private handleBlur_!: SpecificEventListener<'blur'>; // assigned in initialize() - private handleClick_!: SpecificEventListener; // assigned in initialize() - private handleKeydown_!: SpecificEventListener<'keydown'>; // assigned in initialize() - private handleMenuOpened_!: EventListener; // assigned in initialize() - private handleMenuClosed_!: EventListener; // assigned in initialize() - private handleMenuSelected_!: CustomEventListener; // assigned in initialize() - private validationObserver_!: MutationObserver; // assigned in initialize() - - initialize( - labelFactory: LabelFactory = (el) => new MDCFloatingLabel(el), - lineRippleFactory: LineRippleFactory = (el) => new MDCLineRipple(el), - outlineFactory: OutlineFactory = (el) => new MDCNotchedOutline(el), - menuFactory: MenuFactory = (el) => new MDCMenu(el), - iconFactory: IconFactory = (el) => new MDCSelectIcon(el), - helperTextFactory: HelperTextFactory = (el) => new MDCSelectHelperText(el), - ) { - this.nativeControl_ = this.root_.querySelector(strings.NATIVE_CONTROL_SELECTOR); - this.selectedText_ = this.root_.querySelector(strings.SELECTED_TEXT_SELECTOR); - - const targetElement = this.nativeControl_ || this.selectedText_; - if (!targetElement) { - throw new Error( - 'MDCSelect: Missing required element: Exactly one of the following selectors must be present: ' + - `'${strings.NATIVE_CONTROL_SELECTOR}' or '${strings.SELECTED_TEXT_SELECTOR}'`, - ); - } - - this.targetElement_ = targetElement; - if (this.targetElement_.hasAttribute(strings.ARIA_CONTROLS)) { - const helperTextElement = document.getElementById(this.targetElement_.getAttribute(strings.ARIA_CONTROLS)!); - if (helperTextElement) { - this.helperText_ = helperTextFactory(helperTextElement); - } - } - - if (this.selectedText_) { - this.enhancedSelectSetup_(menuFactory); - } - - const labelElement = this.root_.querySelector(strings.LABEL_SELECTOR); - this.label_ = labelElement ? labelFactory(labelElement) : null; - - const lineRippleElement = this.root_.querySelector(strings.LINE_RIPPLE_SELECTOR); - this.lineRipple_ = lineRippleElement ? lineRippleFactory(lineRippleElement) : null; - - const outlineElement = this.root_.querySelector(strings.OUTLINE_SELECTOR); - this.outline_ = outlineElement ? outlineFactory(outlineElement) : null; - - const leadingIcon = this.root_.querySelector(strings.LEADING_ICON_SELECTOR); - if (leadingIcon) { - this.root_.classList.add(cssClasses.WITH_LEADING_ICON); - this.leadingIcon_ = iconFactory(leadingIcon); - - if (this.menuElement_) { - this.menuElement_.classList.add(cssClasses.WITH_LEADING_ICON); - } - } - - if (!this.root_.classList.contains(cssClasses.OUTLINED)) { - this.ripple = this.initRipple_(); - } - - // The required state needs to be sync'd before the mutation observer is added. - this.initialSyncRequiredState_(); - this.addMutationObserverForRequired_(); - } - - /** - * Initializes the select's event listeners and internal state based - * on the environment's state. - */ - initialSyncWithDOM() { - this.handleChange_ = () => this.foundation_.handleChange(/* didChange */ true); - this.handleFocus_ = () => this.foundation_.handleFocus(); - this.handleBlur_ = () => this.foundation_.handleBlur(); - this.handleClick_ = (evt) => { - if (this.selectedText_) { - this.selectedText_.focus(); - } - this.foundation_.handleClick(this.getNormalizedXCoordinate_(evt)); - }; - this.handleKeydown_ = (evt) => this.foundation_.handleKeydown(evt); - this.handleMenuSelected_ = (evtData) => this.selectedIndex = evtData.detail.index; - this.handleMenuOpened_ = () => { - // Menu should open to the last selected element. - if (this.selectedIndex >= 0) { - const selectedItemEl = this.menu_!.items[this.selectedIndex] as HTMLElement; - selectedItemEl.focus(); - } - }; - this.handleMenuClosed_ = () => { - // menuOpened_ is used to track the state of the menu opening or closing since the menu.open function - // will return false if the menu is still closing and this method listens to the closed event which - // occurs after the menu is already closed. - this.menuOpened_ = false; - this.selectedText_!.removeAttribute('aria-expanded'); - if (document.activeElement !== this.selectedText_) { - this.foundation_.handleBlur(); - } - }; - - this.targetElement_.addEventListener('change', this.handleChange_); - this.targetElement_.addEventListener('focus', this.handleFocus_); - this.targetElement_.addEventListener('blur', this.handleBlur_); - - POINTER_EVENTS.forEach((evtType) => { - this.targetElement_.addEventListener(evtType, this.handleClick_ as EventListener); - }); - - if (this.menuElement_) { - this.selectedText_!.addEventListener('keydown', this.handleKeydown_); - this.menu_!.listen(menuSurfaceConstants.strings.CLOSED_EVENT, this.handleMenuClosed_); - this.menu_!.listen(menuSurfaceConstants.strings.OPENED_EVENT, this.handleMenuOpened_); - this.menu_!.listen(menuConstants.strings.SELECTED_EVENT, this.handleMenuSelected_); - - if (this.hiddenInput_ && this.hiddenInput_.value) { - // If the hidden input already has a value, use it to restore the select's value. - // This can happen e.g. if the user goes back or (in some browsers) refreshes the page. - const enhancedAdapterMethods = this.getEnhancedSelectAdapterMethods_(); - enhancedAdapterMethods.setValue(this.hiddenInput_.value); - } else if (this.menuElement_.querySelector(strings.SELECTED_ITEM_SELECTOR)) { - // If an element is selected, the select should set the initial selected text. - const enhancedAdapterMethods = this.getEnhancedSelectAdapterMethods_(); - enhancedAdapterMethods.setValue(enhancedAdapterMethods.getValue()); - } - } - - // Initially sync floating label - this.foundation_.handleChange(/* didChange */ false); - - if (this.root_.classList.contains(cssClasses.DISABLED) - || (this.nativeControl_ && this.nativeControl_.disabled)) { - this.disabled = true; - } - } - - destroy() { - this.targetElement_.removeEventListener('change', this.handleChange_); - this.targetElement_.removeEventListener('focus', this.handleFocus_); - this.targetElement_.removeEventListener('blur', this.handleBlur_); - this.targetElement_.removeEventListener('keydown', this.handleKeydown_); - POINTER_EVENTS.forEach((evtType) => { - this.targetElement_.removeEventListener(evtType, this.handleClick_ as EventListener); - }); - - if (this.menu_) { - this.menu_.unlisten(menuSurfaceConstants.strings.CLOSED_EVENT, this.handleMenuClosed_); - this.menu_.unlisten(menuSurfaceConstants.strings.OPENED_EVENT, this.handleMenuOpened_); - this.menu_.unlisten(menuConstants.strings.SELECTED_EVENT, this.handleMenuSelected_); - this.menu_.destroy(); - } - - if (this.ripple) { - this.ripple.destroy(); - } - if (this.outline_) { - this.outline_.destroy(); - } - if (this.leadingIcon_) { - this.leadingIcon_.destroy(); - } - if (this.helperText_) { - this.helperText_.destroy(); - } - if (this.validationObserver_) { - this.validationObserver_.disconnect(); - } - - super.destroy(); - } - - get value(): string { - return this.foundation_.getValue(); - } - - set value(value: string) { - this.foundation_.setValue(value); - } - - get selectedIndex(): number { - let selectedIndex = -1; - if (this.menuElement_ && this.menu_) { - const selectedEl = this.menuElement_.querySelector(strings.SELECTED_ITEM_SELECTOR)!; - selectedIndex = this.menu_.items.indexOf(selectedEl); - } else if (this.nativeControl_) { - selectedIndex = this.nativeControl_.selectedIndex; - } - return selectedIndex; - } - - set selectedIndex(selectedIndex: number) { - this.foundation_.setSelectedIndex(selectedIndex); - } - - get disabled(): boolean { - return this.root_.classList.contains(cssClasses.DISABLED) || - (this.nativeControl_ ? this.nativeControl_.disabled : false); - } - - set disabled(disabled: boolean) { - this.foundation_.setDisabled(disabled); - } - - set leadingIconAriaLabel(label: string) { - this.foundation_.setLeadingIconAriaLabel(label); - } - - /** - * Sets the text content of the leading icon. - */ - set leadingIconContent(content: string) { - this.foundation_.setLeadingIconContent(content); - } - - /** - * Sets the text content of the helper text. - */ - set helperTextContent(content: string) { - this.foundation_.setHelperTextContent(content); - } - - /** - * Sets the current invalid state of the select. - */ - set valid(isValid: boolean) { - this.foundation_.setValid(isValid); - } - - /** - * Checks if the select is in a valid state. - */ - get valid(): boolean { - return this.foundation_.isValid(); - } - - /** - * Sets the control to the required state. - */ - set required(isRequired: boolean) { - if (this.nativeControl_) { - this.nativeControl_.required = isRequired; - } else { - if (isRequired) { - this.selectedText_!.setAttribute('aria-required', isRequired.toString()); - } else { - this.selectedText_!.removeAttribute('aria-required'); - } - } - } - - /** - * Returns whether the select is required. - */ - get required(): boolean { - if (this.nativeControl_) { - return this.nativeControl_.required; - } else { - return this.selectedText_!.getAttribute('aria-required') === 'true'; - } - } - - /** - * Recomputes the outline SVG path for the outline element. - */ - layout() { - this.foundation_.layout(); - } - - getDefaultFoundation(): MDCSelectFoundation { - return new MDCSelectFoundation({ - ...(this.nativeControl_ ? this.getNativeSelectAdapterMethods_() : this.getEnhancedSelectAdapterMethods_()), - ...this.getCommonAdapterMethods_(), - ...this.getOutlineAdapterMethods_(), - ...this.getLabelAdapterMethods_(), - }, - this.getFoundationMap_(), - ); - } - - /** - * Handles setup for the enhanced menu. - */ - private enhancedSelectSetup_(menuFactory: MenuFactory) { - const isDisabled = this.root_.classList.contains(cssClasses.DISABLED); - this.selectedText_!.setAttribute('tabindex', isDisabled ? '-1' : '0'); - this.hiddenInput_ = this.root_.querySelector(strings.HIDDEN_INPUT_SELECTOR); - this.menuElement_ = this.root_.querySelector(strings.MENU_SELECTOR)!; - this.menu_ = menuFactory(this.menuElement_); - this.menu_.hoistMenuToBody(); - this.menu_.setAnchorElement(this.root_); - this.menu_.setAnchorCorner(menuSurfaceConstants.Corner.BOTTOM_START); - this.menu_.wrapFocus = false; - } - - private initRipple_(): MDCRipple { - // tslint:disable:object-literal-sort-keys - const foundation = new MDCRippleFoundation({ - ...MDCRipple.createAdapter(this), - registerInteractionHandler: (evtType: E, handler: SpecificEventListener) => { - this.targetElement_.addEventListener(evtType, handler); - }, - deregisterInteractionHandler: (evtType: E, handler: SpecificEventListener) => { - this.targetElement_.removeEventListener(evtType, handler); - }, - }); - // tslint:enable:object-literal-sort-keys - - return new MDCRipple(this.root_, foundation); - } - - private getNativeSelectAdapterMethods_() { - // tslint:disable:object-literal-sort-keys - return { - getValue: () => this.nativeControl_!.value, - setValue: (value: string) => { this.nativeControl_!.value = value; }, - openMenu: () => undefined, - closeMenu: () => undefined, - isMenuOpen: () => false, - setSelectedIndex: (index: number) => { this.nativeControl_!.selectedIndex = index; }, - setDisabled: (isDisabled: boolean) => { this.nativeControl_!.disabled = isDisabled; }, - setValid: (isValid: boolean) => { - if (isValid) { - this.root_.classList.remove(cssClasses.INVALID); - } else { - this.root_.classList.add(cssClasses.INVALID); - } - }, - checkValidity: () => this.nativeControl_!.checkValidity(), - }; - // tslint:enable:object-literal-sort-keys - } - - private getEnhancedSelectAdapterMethods_() { - // tslint:disable:object-literal-sort-keys - return { - getValue: () => { - const listItem = this.menuElement_!.querySelector(strings.SELECTED_ITEM_SELECTOR); - if (listItem && listItem.hasAttribute(strings.ENHANCED_VALUE_ATTR)) { - return listItem.getAttribute(strings.ENHANCED_VALUE_ATTR) || ''; - } - return ''; - }, - setValue: (value: string) => { - const element = this.menuElement_!.querySelector(`[${strings.ENHANCED_VALUE_ATTR}="${value}"]`); - this.setEnhancedSelectedIndex_(element ? this.menu_!.items.indexOf(element) : -1); - }, - openMenu: () => { - if (this.menu_ && !this.menu_.open) { - this.menu_.open = true; - this.menuOpened_ = true; - this.selectedText_!.setAttribute('aria-expanded', 'true'); - } - }, - closeMenu: () => { - if (this.menu_ && this.menu_.open) { - this.menu_.open = false; - } - }, - isMenuOpen: () => Boolean(this.menu_) && this.menuOpened_, - setSelectedIndex: (index: number) => this.setEnhancedSelectedIndex_(index), - setDisabled: (isDisabled: boolean) => { - this.selectedText_!.setAttribute('tabindex', isDisabled ? '-1' : '0'); - this.selectedText_!.setAttribute('aria-disabled', isDisabled.toString()); - if (this.hiddenInput_) { - this.hiddenInput_.disabled = isDisabled; - } - }, - checkValidity: () => { - const classList = this.root_.classList; - if (classList.contains(cssClasses.REQUIRED) && !classList.contains(cssClasses.DISABLED)) { - // See notes for required attribute under https://www.w3.org/TR/html52/sec-forms.html#the-select-element - // TL;DR: Invalid if no index is selected, or if the first index is selected and has an empty value. - return this.selectedIndex !== -1 && (this.selectedIndex !== 0 || Boolean(this.value)); - } else { - return true; - } - }, - setValid: (isValid: boolean) => { - this.selectedText_!.setAttribute('aria-invalid', (!isValid).toString()); - if (isValid) { - this.root_.classList.remove(cssClasses.INVALID); - } else { - this.root_.classList.add(cssClasses.INVALID); - } - }, - }; - // tslint:enable:object-literal-sort-keys - } - - private getCommonAdapterMethods_() { - // tslint:disable:object-literal-sort-keys - return { - addClass: (className: string) => this.root_.classList.add(className), - removeClass: (className: string) => this.root_.classList.remove(className), - hasClass: (className: string) => this.root_.classList.contains(className), - setRippleCenter: (normalizedX: number) => this.lineRipple_ && this.lineRipple_.setRippleCenter(normalizedX), - activateBottomLine: () => this.lineRipple_ && this.lineRipple_.activate(), - deactivateBottomLine: () => this.lineRipple_ && this.lineRipple_.deactivate(), - notifyChange: (value: string) => { - const index = this.selectedIndex; - this.emit(strings.CHANGE_EVENT, {value, index}, true /* shouldBubble */); - }, - }; - // tslint:enable:object-literal-sort-keys - } - - private getOutlineAdapterMethods_() { - // tslint:disable:object-literal-sort-keys - return { - hasOutline: () => Boolean(this.outline_), - notchOutline: (labelWidth: number) => this.outline_ && this.outline_.notch(labelWidth), - closeOutline: () => this.outline_ && this.outline_.closeNotch(), - }; - // tslint:enable:object-literal-sort-keys - } - - private getLabelAdapterMethods_() { - return { - floatLabel: (shouldFloat: boolean) => this.label_ && this.label_.float(shouldFloat), - getLabelWidth: () => this.label_ ? this.label_.getWidth() : 0, - }; - } - - /** - * Calculates where the line ripple should start based on the x coordinate within the component. - */ - private getNormalizedXCoordinate_(evt: MouseEvent | TouchEvent): number { - const targetClientRect = (evt.target as Element).getBoundingClientRect(); - const xCoordinate = this.isTouchEvent_(evt) ? evt.touches[0].clientX : evt.clientX; - return xCoordinate - targetClientRect.left; - } - - private isTouchEvent_(evt: MouseEvent | TouchEvent): evt is TouchEvent { - return Boolean((evt as TouchEvent).touches); - } - - /** - * Returns a map of all subcomponents to subfoundations. - */ - private getFoundationMap_(): Partial { - return { - helperText: this.helperText_ ? this.helperText_.foundation : undefined, - leadingIcon: this.leadingIcon_ ? this.leadingIcon_.foundation : undefined, - }; - } - - private setEnhancedSelectedIndex_(index: number) { - const selectedItem = this.menu_!.items[index]; - this.selectedText_!.textContent = selectedItem ? selectedItem.textContent!.trim() : ''; - const previouslySelected = this.menuElement_!.querySelector(strings.SELECTED_ITEM_SELECTOR); - - if (previouslySelected) { - previouslySelected.classList.remove(cssClasses.SELECTED_ITEM_CLASS); - previouslySelected.removeAttribute(strings.ARIA_SELECTED_ATTR); - } - - if (selectedItem) { - selectedItem.classList.add(cssClasses.SELECTED_ITEM_CLASS); - selectedItem.setAttribute(strings.ARIA_SELECTED_ATTR, 'true'); - } - - // Synchronize hidden input's value with data-value attribute of selected item. - // This code path is also followed when setting value directly, so this covers all cases. - if (this.hiddenInput_) { - this.hiddenInput_.value = selectedItem ? selectedItem.getAttribute(strings.ENHANCED_VALUE_ATTR) || '' : ''; - } - - this.layout(); - } - - private initialSyncRequiredState_() { - const isRequired = - (this.targetElement_ as HTMLSelectElement).required - || this.targetElement_.getAttribute('aria-required') === 'true' - || this.root_.classList.contains(cssClasses.REQUIRED); - if (isRequired) { - if (this.nativeControl_) { - this.nativeControl_.required = true; - } else { - this.selectedText_!.setAttribute('aria-required', 'true'); - } - this.root_.classList.add(cssClasses.REQUIRED); - } - } - - private addMutationObserverForRequired_() { - const observerHandler = (attributesList: string[]) => { - attributesList.some((attributeName) => { - if (VALIDATION_ATTR_WHITELIST.indexOf(attributeName) === -1) { - return false; - } - - if (this.selectedText_) { - if (this.selectedText_.getAttribute('aria-required') === 'true') { - this.root_.classList.add(cssClasses.REQUIRED); - } else { - this.root_.classList.remove(cssClasses.REQUIRED); - } - } else { - if (this.nativeControl_!.required) { - this.root_.classList.add(cssClasses.REQUIRED); - } else { - this.root_.classList.remove(cssClasses.REQUIRED); - } - } - - return true; - }); - }; - - const getAttributesList = (mutationsList: MutationRecord[]): string[] => { - return mutationsList - .map((mutation) => mutation.attributeName) - .filter((attributeName) => attributeName) as string[]; - }; - const observer = new MutationObserver((mutationsList) => observerHandler(getAttributesList(mutationsList))); - observer.observe(this.targetElement_, {attributes: true}); - this.validationObserver_ = observer; - } -} - -export {MDCSelect as default, MDCSelect}; -export * from './helper-text'; -export * from './icon'; export * from './adapter'; +export * from './component'; export * from './foundation'; export * from './types'; +export * from './helper-text/index'; +export * from './icon/index'; diff --git a/packages/mdc-select/types.ts b/packages/mdc-select/types.ts index 0d031c687a2..c9acbcba0d1 100644 --- a/packages/mdc-select/types.ts +++ b/packages/mdc-select/types.ts @@ -21,28 +21,21 @@ * THE SOFTWARE. */ -import {MDCFloatingLabel} from '@material/floating-label/index'; -import {MDCLineRipple} from '@material/line-ripple/index'; -import {MDCMenu} from '@material/menu/index'; -import {MDCNotchedOutline} from '@material/notched-outline/index'; -import {MDCSelectHelperText, MDCSelectHelperTextFoundation} from './helper-text/index'; -import {MDCSelectIcon, MDCSelectIconFoundation} from './icon/index'; +import {MDCSelectHelperTextFoundation} from './helper-text/foundation'; +import {MDCSelectIconFoundation} from './icon/foundation'; -export interface FoundationMapType { +export interface MDCSelectFoundationMap { leadingIcon: MDCSelectIconFoundation; helperText: MDCSelectHelperTextFoundation; } -export type SelectEvent = CustomEvent; - -export interface SelectEventDetail { +export interface MDCSelectEventDetail { value: string; index: number; } -export type LineRippleFactory = (el: Element) => MDCLineRipple; -export type HelperTextFactory = (el: Element) => MDCSelectHelperText; -export type MenuFactory = (el: Element) => MDCMenu; -export type IconFactory = (el: Element) => MDCSelectIcon; -export type LabelFactory = (el: Element) => MDCFloatingLabel; -export type OutlineFactory = (el: Element) => MDCNotchedOutline; +// Note: CustomEvent is not supported by Closure Compiler. + +export interface MDCSelectEvent extends Event { + readonly detail: MDCSelectEventDetail; +} diff --git a/packages/mdc-selection-control/index.ts b/packages/mdc-selection-control/index.ts index eb825b19d91..23816fe52ac 100644 --- a/packages/mdc-selection-control/index.ts +++ b/packages/mdc-selection-control/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,15 +21,4 @@ * THE SOFTWARE. */ -import {MDCRipple} from '@material/ripple/index'; - -export interface MDCSelectionControlState { - checked: boolean; - indeterminate: boolean; - disabled: boolean; - value?: string; -} - -export interface MDCSelectionControl { - readonly ripple: MDCRipple | undefined; -} +export * from './types'; diff --git a/typings/custom.d.ts b/packages/mdc-selection-control/types.ts similarity index 78% rename from typings/custom.d.ts rename to packages/mdc-selection-control/types.ts index aa0a20d0fd1..5638dadb6a4 100644 --- a/typings/custom.d.ts +++ b/packages/mdc-selection-control/types.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google Inc. + * Copyright 2017 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,6 +21,15 @@ * THE SOFTWARE. */ -declare interface Window { - CSS: CSS; +import {MDCRipple} from '@material/ripple/component'; + +export interface MDCSelectionControlState { + checked: boolean; + indeterminate: boolean; + disabled: boolean; + value?: string; +} + +export interface MDCSelectionControl { + readonly ripple: MDCRipple | undefined; } diff --git a/packages/mdc-slider/adapter.ts b/packages/mdc-slider/adapter.ts index 797abef5ad8..33bd6b5a55d 100644 --- a/packages/mdc-slider/adapter.ts +++ b/packages/mdc-slider/adapter.ts @@ -21,7 +21,7 @@ * THE SOFTWARE. */ -import {EventType, SpecificEventListener} from '@material/base/index'; +import {EventType, SpecificEventListener} from '@material/base/types'; /** * Defines the shape of the adapter expected by the foundation. @@ -30,7 +30,7 @@ import {EventType, SpecificEventListener} from '@material/base/index'; * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCSliderAdapter { +export interface MDCSliderAdapter { /** * Returns true if className exists for the slider Element */ @@ -156,5 +156,3 @@ interface MDCSliderAdapter { */ isRTL(): boolean; } - -export {MDCSliderAdapter as default, MDCSliderAdapter}; diff --git a/packages/mdc-slider/component.ts b/packages/mdc-slider/component.ts new file mode 100644 index 00000000000..a6c9f10dc9c --- /dev/null +++ b/packages/mdc-slider/component.ts @@ -0,0 +1,186 @@ +/** + * @license + * Copyright 2017 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {MDCSliderAdapter} from './adapter'; +import {strings} from './constants'; +import {MDCSliderFoundation} from './foundation'; + +export class MDCSlider extends MDCComponent { + static attachTo(root: Element) { + return new MDCSlider(root); + } + + protected root_!: HTMLElement; // assigned in MDCComponent constructor + + private thumbContainer_!: HTMLElement; // assigned in initialize() + private track_!: HTMLElement; // assigned in initialize() + private pinValueMarker_!: HTMLElement; // assigned in initialize() + private trackMarkerContainer_!: HTMLElement; // assigned in initialize() + + get value(): number { + return this.foundation_.getValue(); + } + + set value(value: number) { + this.foundation_.setValue(value); + } + + get min(): number { + return this.foundation_.getMin(); + } + + set min(min: number) { + this.foundation_.setMin(min); + } + + get max(): number { + return this.foundation_.getMax(); + } + + set max(max: number) { + this.foundation_.setMax(max); + } + + get step(): number { + return this.foundation_.getStep(); + } + + set step(step: number) { + this.foundation_.setStep(step); + } + + get disabled(): boolean { + return this.foundation_.isDisabled(); + } + + set disabled(disabled: boolean) { + this.foundation_.setDisabled(disabled); + } + + initialize() { + this.thumbContainer_ = this.root_.querySelector(strings.THUMB_CONTAINER_SELECTOR)!; + this.track_ = this.root_.querySelector(strings.TRACK_SELECTOR)!; + this.pinValueMarker_ = this.root_.querySelector(strings.PIN_VALUE_MARKER_SELECTOR)!; + this.trackMarkerContainer_ = this.root_.querySelector(strings.TRACK_MARKER_CONTAINER_SELECTOR)!; + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + // tslint:disable:object-literal-sort-keys + const adapter: MDCSliderAdapter = { + hasClass: (className) => this.root_.classList.contains(className), + addClass: (className) => this.root_.classList.add(className), + removeClass: (className) => this.root_.classList.remove(className), + getAttribute: (name) => this.root_.getAttribute(name), + setAttribute: (name, value) => this.root_.setAttribute(name, value), + removeAttribute: (name) => this.root_.removeAttribute(name), + computeBoundingRect: () => this.root_.getBoundingClientRect(), + getTabIndex: () => this.root_.tabIndex, + registerInteractionHandler: (evtType, handler) => this.listen(evtType, handler), + deregisterInteractionHandler: (evtType, handler) => this.unlisten(evtType, handler), + registerThumbContainerInteractionHandler: (evtType, handler) => { + this.thumbContainer_.addEventListener(evtType, handler); + }, + deregisterThumbContainerInteractionHandler: (evtType, handler) => { + this.thumbContainer_.removeEventListener(evtType, handler); + }, + registerBodyInteractionHandler: (evtType, handler) => document.body.addEventListener(evtType, handler), + deregisterBodyInteractionHandler: (evtType, handler) => document.body.removeEventListener(evtType, handler), + registerResizeHandler: (handler) => window.addEventListener('resize', handler), + deregisterResizeHandler: (handler) => window.removeEventListener('resize', handler), + notifyInput: () => this.emit(strings.INPUT_EVENT, this), // TODO(acdvorak): Create detail interface + notifyChange: () => this.emit(strings.CHANGE_EVENT, this), // TODO(acdvorak): Create detail interface + setThumbContainerStyleProperty: (propertyName, value) => { + this.thumbContainer_.style.setProperty(propertyName, value); + }, + setTrackStyleProperty: (propertyName, value) => this.track_.style.setProperty(propertyName, value), + setMarkerValue: (value) => this.pinValueMarker_.innerText = value.toLocaleString(), + appendTrackMarkers: (numMarkers) => { + const frag = document.createDocumentFragment(); + for (let i = 0; i < numMarkers; i++) { + const marker = document.createElement('div'); + marker.classList.add('mdc-slider__track-marker'); + frag.appendChild(marker); + } + this.trackMarkerContainer_.appendChild(frag); + }, + removeTrackMarkers: () => { + while (this.trackMarkerContainer_.firstChild) { + this.trackMarkerContainer_.removeChild(this.trackMarkerContainer_.firstChild); + } + }, + setLastTrackMarkersStyleProperty: (propertyName, value) => { + // We remove and append new nodes, thus, the last track marker must be dynamically found. + const lastTrackMarker = this.root_.querySelector(strings.LAST_TRACK_MARKER_SELECTOR)!; + lastTrackMarker.style.setProperty(propertyName, value); + }, + isRTL: () => getComputedStyle(this.root_).direction === 'rtl', + }; + // tslint:disable:object-literal-sort-keys + return new MDCSliderFoundation(adapter); + } + + initialSyncWithDOM() { + const origValueNow = this.parseFloat_(this.root_.getAttribute(strings.ARIA_VALUENOW), this.value); + const min = this.parseFloat_(this.root_.getAttribute(strings.ARIA_VALUEMIN), this.min); + const max = this.parseFloat_(this.root_.getAttribute(strings.ARIA_VALUEMAX), this.max); + + // min and max need to be set in the right order to avoid throwing an error + // when the new min is greater than the default max. + if (min >= this.max) { + this.max = max; + this.min = min; + } else { + this.min = min; + this.max = max; + } + + this.step = this.parseFloat_(this.root_.getAttribute(strings.STEP_DATA_ATTR), this.step); + this.value = origValueNow; + this.disabled = ( + this.root_.hasAttribute(strings.ARIA_DISABLED) && + this.root_.getAttribute(strings.ARIA_DISABLED) !== 'false' + ); + this.foundation_.setupTrackMarker(); + } + + layout() { + this.foundation_.layout(); + } + + stepUp(amount = (this.step || 1)) { + this.value += amount; + } + + stepDown(amount = (this.step || 1)) { + this.value -= amount; + } + + private parseFloat_(str: string | null, defaultValue: number) { + const num = parseFloat(str!); // tslint:disable-line:ban + const isNumeric = typeof num === 'number' && isFinite(num); + return isNumeric ? num : defaultValue; + } +} diff --git a/packages/mdc-slider/foundation.ts b/packages/mdc-slider/foundation.ts index 85509fa064d..0d0e8b04aa3 100644 --- a/packages/mdc-slider/foundation.ts +++ b/packages/mdc-slider/foundation.ts @@ -21,9 +21,9 @@ * THE SOFTWARE. */ -import {getCorrectEventName, getCorrectPropertyName} from '@material/animation/index'; +import {getCorrectEventName, getCorrectPropertyName} from '@material/animation/util'; import {MDCFoundation} from '@material/base/foundation'; -import {EventType, SpecificEventListener} from '@material/base/index'; +import {EventType, SpecificEventListener} from '@material/base/types'; import {MDCSliderAdapter} from './adapter'; import {cssClasses, numbers, strings} from './constants'; @@ -56,7 +56,7 @@ const KEY_IDS = { PAGE_UP: 'PageUp', }; -class MDCSliderFoundation extends MDCFoundation { +export class MDCSliderFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } @@ -545,4 +545,4 @@ class MDCSliderFoundation extends MDCFoundation { } } -export {MDCSliderFoundation as default, MDCSliderFoundation}; +export default MDCSliderFoundation; diff --git a/packages/mdc-slider/index.ts b/packages/mdc-slider/index.ts index f991880180a..f8c89ac94f3 100644 --- a/packages/mdc-slider/index.ts +++ b/packages/mdc-slider/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,166 +21,6 @@ * THE SOFTWARE. */ -import {MDCComponent} from '@material/base/component'; -import {strings} from './constants'; -import {MDCSliderFoundation} from './foundation'; - -class MDCSlider extends MDCComponent { - static attachTo(root: Element) { - return new MDCSlider(root); - } - - protected root_!: HTMLElement; // assigned in MDCComponent constructor - - private thumbContainer_!: HTMLElement; // assigned in initialize() - private track_!: HTMLElement; // assigned in initialize() - private pinValueMarker_!: HTMLElement; // assigned in initialize() - private trackMarkerContainer_!: HTMLElement; // assigned in initialize() - - get value(): number { - return this.foundation_.getValue(); - } - - set value(value: number) { - this.foundation_.setValue(value); - } - - get min(): number { - return this.foundation_.getMin(); - } - - set min(min: number) { - this.foundation_.setMin(min); - } - - get max(): number { - return this.foundation_.getMax(); - } - - set max(max: number) { - this.foundation_.setMax(max); - } - - get step(): number { - return this.foundation_.getStep(); - } - - set step(step: number) { - this.foundation_.setStep(step); - } - - get disabled(): boolean { - return this.foundation_.isDisabled(); - } - - set disabled(disabled: boolean) { - this.foundation_.setDisabled(disabled); - } - - initialize() { - this.thumbContainer_ = this.root_.querySelector(strings.THUMB_CONTAINER_SELECTOR)!; - this.track_ = this.root_.querySelector(strings.TRACK_SELECTOR)!; - this.pinValueMarker_ = this.root_.querySelector(strings.PIN_VALUE_MARKER_SELECTOR)!; - this.trackMarkerContainer_ = this.root_.querySelector(strings.TRACK_MARKER_CONTAINER_SELECTOR)!; - } - - getDefaultFoundation(): MDCSliderFoundation { - // tslint:disable:object-literal-sort-keys - return new MDCSliderFoundation({ - hasClass: (className) => this.root_.classList.contains(className), - addClass: (className) => this.root_.classList.add(className), - removeClass: (className) => this.root_.classList.remove(className), - getAttribute: (name) => this.root_.getAttribute(name), - setAttribute: (name, value) => this.root_.setAttribute(name, value), - removeAttribute: (name) => this.root_.removeAttribute(name), - computeBoundingRect: () => this.root_.getBoundingClientRect(), - getTabIndex: () => this.root_.tabIndex, - registerInteractionHandler: (evtType, handler) => this.root_.addEventListener(evtType, handler), - deregisterInteractionHandler: (evtType, handler) => this.root_.removeEventListener(evtType, handler), - registerThumbContainerInteractionHandler: (evtType, handler) => { - this.thumbContainer_.addEventListener(evtType, handler); - }, - deregisterThumbContainerInteractionHandler: (evtType, handler) => { - this.thumbContainer_.removeEventListener(evtType, handler); - }, - registerBodyInteractionHandler: (evtType, handler) => document.body.addEventListener(evtType, handler), - deregisterBodyInteractionHandler: (evtType, handler) => document.body.removeEventListener(evtType, handler), - registerResizeHandler: (handler) => window.addEventListener('resize', handler), - deregisterResizeHandler: (handler) => window.removeEventListener('resize', handler), - notifyInput: () => this.emit(strings.INPUT_EVENT, this), - notifyChange: () => this.emit(strings.CHANGE_EVENT, this), - setThumbContainerStyleProperty: (propertyName, value) => { - this.thumbContainer_.style.setProperty(propertyName, value); - }, - setTrackStyleProperty: (propertyName, value) => this.track_.style.setProperty(propertyName, value), - setMarkerValue: (value) => this.pinValueMarker_.innerText = value.toLocaleString(), - appendTrackMarkers: (numMarkers) => { - const frag = document.createDocumentFragment(); - for (let i = 0; i < numMarkers; i++) { - const marker = document.createElement('div'); - marker.classList.add('mdc-slider__track-marker'); - frag.appendChild(marker); - } - this.trackMarkerContainer_.appendChild(frag); - }, - removeTrackMarkers: () => { - while (this.trackMarkerContainer_.firstChild) { - this.trackMarkerContainer_.removeChild(this.trackMarkerContainer_.firstChild); - } - }, - setLastTrackMarkersStyleProperty: (propertyName, value) => { - // We remove and append new nodes, thus, the last track marker must be dynamically found. - const lastTrackMarker = this.root_.querySelector(strings.LAST_TRACK_MARKER_SELECTOR)!; - lastTrackMarker.style.setProperty(propertyName, value); - }, - isRTL: () => getComputedStyle(this.root_).direction === 'rtl', - }); - // tslint:disable:object-literal-sort-keys - } - - initialSyncWithDOM() { - const origValueNow = this.parseFloat_(this.root_.getAttribute(strings.ARIA_VALUENOW), this.value); - const min = this.parseFloat_(this.root_.getAttribute(strings.ARIA_VALUEMIN), this.min); - const max = this.parseFloat_(this.root_.getAttribute(strings.ARIA_VALUEMAX), this.max); - - // min and max need to be set in the right order to avoid throwing an error - // when the new min is greater than the default max. - if (min >= this.max) { - this.max = max; - this.min = min; - } else { - this.min = min; - this.max = max; - } - - this.step = this.parseFloat_(this.root_.getAttribute(strings.STEP_DATA_ATTR), this.step); - this.value = origValueNow; - this.disabled = ( - this.root_.hasAttribute(strings.ARIA_DISABLED) && - this.root_.getAttribute(strings.ARIA_DISABLED) !== 'false' - ); - this.foundation_.setupTrackMarker(); - } - - layout() { - this.foundation_.layout(); - } - - stepUp(amount = (this.step || 1)) { - this.value += amount; - } - - stepDown(amount = (this.step || 1)) { - this.value -= amount; - } - - private parseFloat_(str: string | null, defaultValue: number) { - const num = parseFloat(str!); // tslint:disable-line:ban - const isNumeric = typeof num === 'number' && isFinite(num); - return isNumeric ? num : defaultValue; - } -} - -export {MDCSlider as default, MDCSlider}; export * from './adapter'; +export * from './component'; export * from './foundation'; diff --git a/packages/mdc-snackbar/adapter.ts b/packages/mdc-snackbar/adapter.ts index e450d031154..188e44a5021 100644 --- a/packages/mdc-snackbar/adapter.ts +++ b/packages/mdc-snackbar/adapter.ts @@ -28,7 +28,7 @@ * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCSnackbarAdapter { +export interface MDCSnackbarAdapter { addClass(className: string): void; announce(): void; notifyClosed(reason: string): void; @@ -37,5 +37,3 @@ interface MDCSnackbarAdapter { notifyOpening(): void; removeClass(className: string): void; } - -export {MDCSnackbarAdapter as default, MDCSnackbarAdapter}; diff --git a/packages/mdc-snackbar/component.ts b/packages/mdc-snackbar/component.ts new file mode 100644 index 00000000000..1b1a5834493 --- /dev/null +++ b/packages/mdc-snackbar/component.ts @@ -0,0 +1,170 @@ +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {SpecificEventListener} from '@material/base/types'; +import {ponyfill} from '@material/dom/index'; +import {MDCSnackbarAdapter} from './adapter'; +import {strings} from './constants'; +import {MDCSnackbarFoundation} from './foundation'; +import {MDCSnackbarAnnouncer, MDCSnackbarAnnouncerFactory, MDCSnackbarCloseEventDetail} from './types'; +import * as util from './util'; + +const { + SURFACE_SELECTOR, LABEL_SELECTOR, ACTION_SELECTOR, DISMISS_SELECTOR, + OPENING_EVENT, OPENED_EVENT, CLOSING_EVENT, CLOSED_EVENT, +} = strings; + +export class MDCSnackbar extends MDCComponent { + static attachTo(root: Element) { + return new MDCSnackbar(root); + } + + private announce_!: MDCSnackbarAnnouncer; // assigned in initialize() + + private actionEl_!: Element; // assigned in initialSyncWithDOM() + private labelEl_!: Element; // assigned in initialSyncWithDOM() + private surfaceEl_!: Element; // assigned in initialSyncWithDOM() + + private handleKeyDown_!: SpecificEventListener<'keydown'>; // assigned in initialSyncWithDOM() + private handleSurfaceClick_!: SpecificEventListener<'click'>; // assigned in initialSyncWithDOM() + + initialize(announcerFactory: MDCSnackbarAnnouncerFactory = () => util.announce) { + this.announce_ = announcerFactory(); + } + + initialSyncWithDOM() { + this.surfaceEl_ = this.root_.querySelector(SURFACE_SELECTOR)!; + this.labelEl_ = this.root_.querySelector(LABEL_SELECTOR)!; + this.actionEl_ = this.root_.querySelector(ACTION_SELECTOR)!; + + this.handleKeyDown_ = (evt) => this.foundation_.handleKeyDown(evt); + this.handleSurfaceClick_ = (evt) => { + const target = evt.target as Element; + if (this.isActionButton_(target)) { + this.foundation_.handleActionButtonClick(evt); + } else if (this.isActionIcon_(target)) { + this.foundation_.handleActionIconClick(evt); + } + }; + + this.registerKeyDownHandler_(this.handleKeyDown_); + this.registerSurfaceClickHandler_(this.handleSurfaceClick_); + } + + destroy() { + super.destroy(); + this.deregisterKeyDownHandler_(this.handleKeyDown_); + this.deregisterSurfaceClickHandler_(this.handleSurfaceClick_); + } + + open() { + this.foundation_.open(); + } + + /** + * @param reason Why the snackbar was closed. Value will be passed to CLOSING_EVENT and CLOSED_EVENT via the + * `event.detail.reason` property. Standard values are REASON_ACTION and REASON_DISMISS, but custom + * client-specific values may also be used if desired. + */ + close(reason = '') { + this.foundation_.close(reason); + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + const adapter: MDCSnackbarAdapter = { + addClass: (className) => this.root_.classList.add(className), + announce: () => this.announce_(this.labelEl_), + notifyClosed: (reason) => this.emit(CLOSED_EVENT, reason ? {reason} : {}), + notifyClosing: (reason) => this.emit(CLOSING_EVENT, reason ? {reason} : {}), + notifyOpened: () => this.emit(OPENED_EVENT, {}), + notifyOpening: () => this.emit(OPENING_EVENT, {}), + removeClass: (className) => this.root_.classList.remove(className), + }; + return new MDCSnackbarFoundation(adapter); + } + + get timeoutMs(): number { + return this.foundation_.getTimeoutMs(); + } + + set timeoutMs(timeoutMs: number) { + this.foundation_.setTimeoutMs(timeoutMs); + } + + get closeOnEscape(): boolean { + return this.foundation_.getCloseOnEscape(); + } + + set closeOnEscape(closeOnEscape: boolean) { + this.foundation_.setCloseOnEscape(closeOnEscape); + } + + get isOpen(): boolean { + return this.foundation_.isOpen(); + } + + get labelText(): string { + // This property only returns null if the node is a document, DOCTYPE, or notation. + // On Element nodes, it always returns a string. + return this.labelEl_.textContent!; + } + + set labelText(labelText: string) { + this.labelEl_.textContent = labelText; + } + + get actionButtonText(): string { + return this.actionEl_.textContent!; + } + + set actionButtonText(actionButtonText: string) { + this.actionEl_.textContent = actionButtonText; + } + + private registerKeyDownHandler_(handler: SpecificEventListener<'keydown'>) { + this.listen('keydown', handler); + } + + private deregisterKeyDownHandler_(handler: SpecificEventListener<'keydown'>) { + this.unlisten('keydown', handler); + } + + private registerSurfaceClickHandler_(handler: SpecificEventListener<'click'>) { + this.surfaceEl_.addEventListener('click', handler as EventListener); + } + + private deregisterSurfaceClickHandler_(handler: SpecificEventListener<'click'>) { + this.surfaceEl_.removeEventListener('click', handler as EventListener); + } + + private isActionButton_(target: Element): boolean { + return Boolean(ponyfill.closest(target, ACTION_SELECTOR)); + } + + private isActionIcon_(target: Element): boolean { + return Boolean(ponyfill.closest(target, DISMISS_SELECTOR)); + } +} diff --git a/packages/mdc-snackbar/foundation.ts b/packages/mdc-snackbar/foundation.ts index e2d9931ce5c..37051c139af 100644 --- a/packages/mdc-snackbar/foundation.ts +++ b/packages/mdc-snackbar/foundation.ts @@ -28,7 +28,7 @@ import {cssClasses, numbers, strings} from './constants'; const {OPENING, OPEN, CLOSING} = cssClasses; const {REASON_ACTION, REASON_DISMISS} = strings; -class MDCSnackbarFoundation extends MDCFoundation { +export class MDCSnackbarFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } @@ -192,4 +192,4 @@ class MDCSnackbarFoundation extends MDCFoundation { } } -export {MDCSnackbarFoundation as default, MDCSnackbarFoundation}; +export default MDCSnackbarFoundation; diff --git a/packages/mdc-snackbar/index.ts b/packages/mdc-snackbar/index.ts index 78875bea30c..3de836c29e1 100644 --- a/packages/mdc-snackbar/index.ts +++ b/packages/mdc-snackbar/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2018 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,151 +21,10 @@ * THE SOFTWARE. */ -import {MDCComponent} from '@material/base/component'; -import {SpecificEventListener} from '@material/base/index'; -import {ponyfill} from '@material/dom/index'; -import {strings} from './constants'; -import {MDCSnackbarFoundation} from './foundation'; -import {Announcer, AnnouncerFactory} from './types'; import * as util from './util'; -const { - SURFACE_SELECTOR, LABEL_SELECTOR, ACTION_SELECTOR, DISMISS_SELECTOR, - OPENING_EVENT, OPENED_EVENT, CLOSING_EVENT, CLOSED_EVENT, -} = strings; - -class MDCSnackbar extends MDCComponent { - static attachTo(root: Element) { - return new MDCSnackbar(root); - } - - private announce_!: Announcer; // assigned in initialize() - - private actionEl_!: Element; // assigned in initialSyncWithDOM() - private labelEl_!: Element; // assigned in initialSyncWithDOM() - private surfaceEl_!: Element; // assigned in initialSyncWithDOM() - - private handleKeyDown_!: SpecificEventListener<'keydown'>; // assigned in initialSyncWithDOM() - private handleSurfaceClick_!: SpecificEventListener<'click'>; // assigned in initialSyncWithDOM() - - initialize(announcerFactory: AnnouncerFactory = () => util.announce) { - this.announce_ = announcerFactory(); - } - - initialSyncWithDOM() { - this.surfaceEl_ = this.root_.querySelector(SURFACE_SELECTOR)!; - this.labelEl_ = this.root_.querySelector(LABEL_SELECTOR)!; - this.actionEl_ = this.root_.querySelector(ACTION_SELECTOR)!; - - this.handleKeyDown_ = (evt) => this.foundation_.handleKeyDown(evt); - this.handleSurfaceClick_ = (evt) => { - const target = evt.target as Element; - if (this.isActionButton_(target)) { - this.foundation_.handleActionButtonClick(evt); - } else if (this.isActionIcon_(target)) { - this.foundation_.handleActionIconClick(evt); - } - }; - - this.registerKeyDownHandler_(this.handleKeyDown_); - this.registerSurfaceClickHandler_(this.handleSurfaceClick_); - } - - destroy() { - super.destroy(); - this.deregisterKeyDownHandler_(this.handleKeyDown_); - this.deregisterSurfaceClickHandler_(this.handleSurfaceClick_); - } - - open() { - this.foundation_.open(); - } - - /** - * @param reason Why the snackbar was closed. Value will be passed to CLOSING_EVENT and CLOSED_EVENT via the - * `event.detail.reason` property. Standard values are REASON_ACTION and REASON_DISMISS, but custom - * client-specific values may also be used if desired. - */ - close(reason = '') { - this.foundation_.close(reason); - } - - getDefaultFoundation() { - return new MDCSnackbarFoundation({ - addClass: (className) => this.root_.classList.add(className), - announce: () => this.announce_(this.labelEl_), - notifyClosed: (reason) => this.emit(CLOSED_EVENT, reason ? {reason} : {}), - notifyClosing: (reason) => this.emit(CLOSING_EVENT, reason ? {reason} : {}), - notifyOpened: () => this.emit(OPENED_EVENT, {}), - notifyOpening: () => this.emit(OPENING_EVENT, {}), - removeClass: (className) => this.root_.classList.remove(className), - }); - } - - get timeoutMs(): number { - return this.foundation_.getTimeoutMs(); - } - - set timeoutMs(timeoutMs: number) { - this.foundation_.setTimeoutMs(timeoutMs); - } - - get closeOnEscape(): boolean { - return this.foundation_.getCloseOnEscape(); - } - - set closeOnEscape(closeOnEscape: boolean) { - this.foundation_.setCloseOnEscape(closeOnEscape); - } - - get isOpen(): boolean { - return this.foundation_.isOpen(); - } - - get labelText(): string { - // This property only returns null if the node is a document, DOCTYPE, or notation. - // On Element nodes, it always returns a string. - return this.labelEl_.textContent!; - } - - set labelText(labelText: string) { - this.labelEl_.textContent = labelText; - } - - get actionButtonText(): string { - return this.actionEl_.textContent!; - } - - set actionButtonText(actionButtonText: string) { - this.actionEl_.textContent = actionButtonText; - } - - private registerKeyDownHandler_(handler: SpecificEventListener<'keydown'>) { - this.listen('keydown', handler); - } - - private deregisterKeyDownHandler_(handler: SpecificEventListener<'keydown'>) { - this.unlisten('keydown', handler); - } - - private registerSurfaceClickHandler_(handler: SpecificEventListener<'click'>) { - this.surfaceEl_.addEventListener('click', handler as EventListener); - } - - private deregisterSurfaceClickHandler_(handler: SpecificEventListener<'click'>) { - this.surfaceEl_.removeEventListener('click', handler as EventListener); - } - - private isActionButton_(target: Element): boolean { - return Boolean(ponyfill.closest(target, ACTION_SELECTOR)); - } - - private isActionIcon_(target: Element): boolean { - return Boolean(ponyfill.closest(target, DISMISS_SELECTOR)); - } -} - -export {MDCSnackbar as default, MDCSnackbar, util}; +export {util}; export * from './adapter'; +export * from './component'; export * from './foundation'; export * from './types'; diff --git a/packages/mdc-snackbar/types.ts b/packages/mdc-snackbar/types.ts index e5ed7555b44..eb0cfdcb815 100644 --- a/packages/mdc-snackbar/types.ts +++ b/packages/mdc-snackbar/types.ts @@ -21,5 +21,15 @@ * THE SOFTWARE. */ -export type Announcer = (ariaEl: Element, labelEl?: Element) => void; -export type AnnouncerFactory = () => Announcer; +export type MDCSnackbarAnnouncer = (ariaEl: Element, labelEl?: Element) => void; +export type MDCSnackbarAnnouncerFactory = () => MDCSnackbarAnnouncer; + +export interface MDCSnackbarCloseEventDetail { + reason?: string; +} + +// Note: CustomEvent is not supported by Closure Compiler. + +export interface MDCSnackbarCloseEvent extends Event { + readonly detail: MDCSnackbarCloseEventDetail; +} diff --git a/packages/mdc-switch/adapter.ts b/packages/mdc-switch/adapter.ts index 2b3ba952533..22564cd4967 100644 --- a/packages/mdc-switch/adapter.ts +++ b/packages/mdc-switch/adapter.ts @@ -28,7 +28,7 @@ * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCSwitchAdapter { +export interface MDCSwitchAdapter { /** * Adds a CSS class to the root element. */ @@ -49,5 +49,3 @@ interface MDCSwitchAdapter { */ setNativeControlDisabled(disabled: boolean): void; } - -export {MDCSwitchAdapter as default, MDCSwitchAdapter}; diff --git a/packages/mdc-switch/component.ts b/packages/mdc-switch/component.ts new file mode 100644 index 00000000000..47d1a57a1ab --- /dev/null +++ b/packages/mdc-switch/component.ts @@ -0,0 +1,129 @@ +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {EventType} from '@material/base/types'; +import {ponyfill} from '@material/dom/index'; +import {MDCRipple, MDCRippleAdapter, MDCRippleCapableSurface, MDCRippleFoundation} from '@material/ripple/index'; +import {MDCSelectionControl} from '@material/selection-control/index'; +import {MDCSwitchAdapter} from './adapter'; +import {MDCSwitchFoundation} from './foundation'; + +/** + * An implementation of the switch component defined by the Material Design spec. + * + * https://material.io/design/components/selection-controls.html#switches + */ +export class MDCSwitch extends MDCComponent + implements MDCSelectionControl, MDCRippleCapableSurface { + static attachTo(root: HTMLElement) { + return new MDCSwitch(root); + } + + // Public visibility for this property is required by MDCRippleCapableSurface. + root_!: Element; // assigned in MDCComponent constructor + + private readonly ripple_ = this.createRipple_(); + + // Initialized in `initialSyncWithDOM`. + private changeHandler_!: EventListener; + + destroy() { + super.destroy(); + this.ripple_.destroy(); + this.nativeControl_.removeEventListener('change', this.changeHandler_); + } + + initialSyncWithDOM() { + this.changeHandler_ = (...args) => this.foundation_.handleChange(...args); + this.nativeControl_.addEventListener('change', this.changeHandler_); + + // Sometimes the checked state of the input element is saved in the history. + // The switch styling should match the checked state of the input element. + // Do an initial sync between the native control and the foundation. + this.checked = this.checked; + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + const adapter: MDCSwitchAdapter = { + addClass: (className) => this.root_.classList.add(className), + removeClass: (className) => this.root_.classList.remove(className), + setNativeControlChecked: (checked) => this.nativeControl_.checked = checked, + setNativeControlDisabled: (disabled) => this.nativeControl_.disabled = disabled, + }; + return new MDCSwitchFoundation(adapter); + } + + get ripple() { + return this.ripple_; + } + + get checked() { + return this.nativeControl_.checked; + } + + set checked(checked) { + this.foundation_.setChecked(checked); + } + + get disabled() { + return this.nativeControl_.disabled; + } + + set disabled(disabled) { + this.foundation_.setDisabled(disabled); + } + + private createRipple_(): MDCRipple { + const {RIPPLE_SURFACE_SELECTOR} = MDCSwitchFoundation.strings; + const rippleSurface = this.root_.querySelector(RIPPLE_SURFACE_SELECTOR) as HTMLElement; + + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + const adapter: MDCRippleAdapter = { + ...MDCRipple.createAdapter(this), + addClass: (className: string) => rippleSurface.classList.add(className), + computeBoundingRect: () => rippleSurface.getBoundingClientRect(), + deregisterInteractionHandler: (evtType: EventType, handler: EventListener) => { + this.nativeControl_.removeEventListener(evtType, handler); + }, + isSurfaceActive: () => ponyfill.matches(this.nativeControl_, ':active'), + isUnbounded: () => true, + registerInteractionHandler: (evtType: EventType, handler: EventListener) => { + this.nativeControl_.addEventListener(evtType, handler); + }, + removeClass: (className: string) => rippleSurface.classList.remove(className), + updateCssVariable: (varName: string, value: string) => { + rippleSurface.style.setProperty(varName, value); + }, + }; + return new MDCRipple(this.root_, new MDCRippleFoundation(adapter)); + } + + private get nativeControl_() { + const {NATIVE_CONTROL_SELECTOR} = MDCSwitchFoundation.strings; + return this.root_.querySelector(NATIVE_CONTROL_SELECTOR) as HTMLInputElement; + } +} diff --git a/packages/mdc-switch/foundation.ts b/packages/mdc-switch/foundation.ts index 19bdd92e842..ba3ceb93de1 100644 --- a/packages/mdc-switch/foundation.ts +++ b/packages/mdc-switch/foundation.ts @@ -31,7 +31,7 @@ import {cssClasses, strings} from './constants'; * See architecture documentation for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -class MDCSwitchFoundation extends MDCFoundation { +export class MDCSwitchFoundation extends MDCFoundation { /** The string constants used by the switch. */ static get strings() { return strings; @@ -88,4 +88,4 @@ class MDCSwitchFoundation extends MDCFoundation { } } -export {MDCSwitchFoundation as default, MDCSwitchFoundation}; +export default MDCSwitchFoundation; diff --git a/packages/mdc-switch/index.ts b/packages/mdc-switch/index.ts index 593a666b2d1..f8c89ac94f3 100644 --- a/packages/mdc-switch/index.ts +++ b/packages/mdc-switch/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2018 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,105 +21,6 @@ * THE SOFTWARE. */ -import {MDCComponent} from '@material/base/component'; -import {EventType} from '@material/base/index'; -import {ponyfill} from '@material/dom/index'; -import {MDCRipple, MDCRippleFoundation, RippleCapableSurface} from '@material/ripple/index'; -import {MDCSelectionControl} from '@material/selection-control/index'; -import {MDCSwitchFoundation} from './foundation'; - -/** - * An implementation of the switch component defined by the Material Design spec. - * - * https://material.io/design/components/selection-controls.html#switches - */ -class MDCSwitch extends MDCComponent implements MDCSelectionControl, RippleCapableSurface { - static attachTo(root: HTMLElement) { - return new MDCSwitch(root); - } - - // Public visibility for this property is required by RippleCapableSurface. - root_!: Element; // assigned in MDCComponent constructor - - private ripple_ = this.initRipple_(); - - // Initialized in `initialSyncWithDOM`. - private changeHandler_!: EventListener; - - destroy() { - super.destroy(); - this.ripple_.destroy(); - this.nativeControl_.removeEventListener('change', this.changeHandler_); - } - - initialSyncWithDOM() { - this.changeHandler_ = (...args) => this.foundation_.handleChange(...args); - this.nativeControl_.addEventListener('change', this.changeHandler_); - - // Sometimes the checked state of the input element is saved in the history. - // The switch styling should match the checked state of the input element. - // Do an initial sync between the native control and the foundation. - this.checked = this.checked; - } - - getDefaultFoundation() { - return new MDCSwitchFoundation({ - addClass: (className: string) => this.root_.classList.add(className), - removeClass: (className: string) => this.root_.classList.remove(className), - setNativeControlChecked: (checked: boolean) => this.nativeControl_.checked = checked, - setNativeControlDisabled: (disabled: boolean) => this.nativeControl_.disabled = disabled, - }); - } - - get ripple() { - return this.ripple_; - } - - get checked() { - return this.nativeControl_.checked; - } - - set checked(checked) { - this.foundation_.setChecked(checked); - } - - get disabled() { - return this.nativeControl_.disabled; - } - - set disabled(disabled) { - this.foundation_.setDisabled(disabled); - } - - private initRipple_() { - const {RIPPLE_SURFACE_SELECTOR} = MDCSwitchFoundation.strings; - const rippleSurface = this.root_.querySelector(RIPPLE_SURFACE_SELECTOR) as HTMLElement; - - return new MDCRipple(this.root_, new MDCRippleFoundation({ - ...MDCRipple.createAdapter(this), - addClass: (className: string) => rippleSurface.classList.add(className), - computeBoundingRect: () => rippleSurface.getBoundingClientRect(), - deregisterInteractionHandler: (evtType: EventType, handler: EventListener) => { - this.nativeControl_.removeEventListener(evtType, handler); - }, - isSurfaceActive: () => ponyfill.matches(this.nativeControl_, ':active'), - isUnbounded: () => true, - registerInteractionHandler: (evtType: EventType, handler: EventListener) => { - this.nativeControl_.addEventListener(evtType, handler); - }, - removeClass: (className: string) => rippleSurface.classList.remove(className), - updateCssVariable: (varName: string, value: string) => { - rippleSurface.style.setProperty(varName, value); - }, - })); - } - - private get nativeControl_() { - const {NATIVE_CONTROL_SELECTOR} = MDCSwitchFoundation.strings; - return this.root_.querySelector(NATIVE_CONTROL_SELECTOR) as HTMLInputElement; - } -} - -export {MDCSwitch as default, MDCSwitch}; export * from './adapter'; +export * from './component'; export * from './foundation'; diff --git a/packages/mdc-tab-bar/adapter.ts b/packages/mdc-tab-bar/adapter.ts index eb648f3d3f3..785911e92dc 100644 --- a/packages/mdc-tab-bar/adapter.ts +++ b/packages/mdc-tab-bar/adapter.ts @@ -30,7 +30,7 @@ import {MDCTabDimensions} from '@material/tab/types'; * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCTabBarAdapter { +export interface MDCTabBarAdapter { /** * Scrolls to the given position * @param scrollX The position to scroll to @@ -127,5 +127,3 @@ interface MDCTabBarAdapter { */ notifyTabActivated(index: number): void; } - -export {MDCTabBarAdapter as default, MDCTabBarAdapter}; diff --git a/packages/mdc-tab-bar/component.ts b/packages/mdc-tab-bar/component.ts new file mode 100644 index 00000000000..97a0f4b1a45 --- /dev/null +++ b/packages/mdc-tab-bar/component.ts @@ -0,0 +1,177 @@ +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {CustomEventListener, SpecificEventListener} from '@material/base/types'; +import {MDCTabScroller, MDCTabScrollerFactory} from '@material/tab-scroller/index'; +import {MDCTab, MDCTabFactory, MDCTabFoundation, MDCTabInteractionEvent} from '@material/tab/index'; +import {MDCTabBarAdapter} from './adapter'; +import {MDCTabBarFoundation} from './foundation'; +import {MDCTabBarActivatedEventDetail} from './types'; + +const {strings} = MDCTabBarFoundation; + +let tabIdCounter = 0; + +export class MDCTabBar extends MDCComponent { + static attachTo(root: Element): MDCTabBar { + return new MDCTabBar(root); + } + + private tabList_!: MDCTab[]; // assigned in initialize() + private tabScroller_!: MDCTabScroller | null; // assigned in initialize() + private handleTabInteraction_!: CustomEventListener; // assigned in initialSyncWithDOM() + private handleKeyDown_!: SpecificEventListener<'keydown'>; // assigned in initialSyncWithDOM() + + set focusOnActivate(focusOnActivate: boolean) { + this.tabList_.forEach((tab) => tab.focusOnActivate = focusOnActivate); + } + + set useAutomaticActivation(useAutomaticActivation: boolean) { + this.foundation_.setUseAutomaticActivation(useAutomaticActivation); + } + + initialize( + tabFactory: MDCTabFactory = (el) => new MDCTab(el), + tabScrollerFactory: MDCTabScrollerFactory = (el) => new MDCTabScroller(el), + ) { + this.tabList_ = this.instantiateTabs_(tabFactory); + this.tabScroller_ = this.instantiateTabScroller_(tabScrollerFactory); + } + + initialSyncWithDOM() { + this.handleTabInteraction_ = (evt) => this.foundation_.handleTabInteraction(evt); + this.handleKeyDown_ = (evt) => this.foundation_.handleKeyDown(evt); + + this.listen(MDCTabFoundation.strings.INTERACTED_EVENT, this.handleTabInteraction_); + this.listen('keydown', this.handleKeyDown_); + + for (let i = 0; i < this.tabList_.length; i++) { + if (this.tabList_[i].active) { + this.scrollIntoView(i); + break; + } + } + } + + destroy() { + super.destroy(); + this.unlisten(MDCTabFoundation.strings.INTERACTED_EVENT, this.handleTabInteraction_); + this.unlisten('keydown', this.handleKeyDown_); + this.tabList_.forEach((tab) => tab.destroy()); + + if (this.tabScroller_) { + this.tabScroller_.destroy(); + } + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + // tslint:disable:object-literal-sort-keys + const adapter: MDCTabBarAdapter = { + scrollTo: (scrollX) => this.tabScroller_!.scrollTo(scrollX), + incrementScroll: (scrollXIncrement) => this.tabScroller_!.incrementScroll(scrollXIncrement), + getScrollPosition: () => this.tabScroller_!.getScrollPosition(), + getScrollContentWidth: () => this.tabScroller_!.getScrollContentWidth(), + getOffsetWidth: () => (this.root_ as HTMLElement).offsetWidth, + isRTL: () => window.getComputedStyle(this.root_).getPropertyValue('direction') === 'rtl', + setActiveTab: (index) => this.foundation_.activateTab(index), + activateTabAtIndex: (index, clientRect) => this.tabList_[index].activate(clientRect), + deactivateTabAtIndex: (index) => this.tabList_[index].deactivate(), + focusTabAtIndex: (index) => this.tabList_[index].focus(), + getTabIndicatorClientRectAtIndex: (index) => this.tabList_[index].computeIndicatorClientRect(), + getTabDimensionsAtIndex: (index) => this.tabList_[index].computeDimensions(), + getPreviousActiveTabIndex: () => { + for (let i = 0; i < this.tabList_.length; i++) { + if (this.tabList_[i].active) { + return i; + } + } + return -1; + }, + getFocusedTabIndex: () => { + const tabElements = this.getTabElements_(); + const activeElement = document.activeElement!; + return tabElements.indexOf(activeElement); + }, + getIndexOfTabById: (id) => { + for (let i = 0; i < this.tabList_.length; i++) { + if (this.tabList_[i].id === id) { + return i; + } + } + return -1; + }, + getTabListLength: () => this.tabList_.length, + notifyTabActivated: (index) => + this.emit(strings.TAB_ACTIVATED_EVENT, {index}, true), + }; + // tslint:enable:object-literal-sort-keys + return new MDCTabBarFoundation(adapter); + } + + /** + * Activates the tab at the given index + * @param index The index of the tab + */ + activateTab(index: number) { + this.foundation_.activateTab(index); + } + + /** + * Scrolls the tab at the given index into view + * @param index THe index of the tab + */ + scrollIntoView(index: number) { + this.foundation_.scrollIntoView(index); + } + + /** + * Returns all the tab elements in a nice clean array + */ + private getTabElements_(): Element[] { + return [].slice.call(this.root_.querySelectorAll(strings.TAB_SELECTOR)); + } + + /** + * Instantiates tab components on all child tab elements + */ + private instantiateTabs_(tabFactory: MDCTabFactory) { + return this.getTabElements_().map((el) => { + el.id = el.id || `mdc-tab-${++tabIdCounter}`; + return tabFactory(el); + }); + } + + /** + * Instantiates tab scroller component on the child tab scroller element + */ + private instantiateTabScroller_(tabScrollerFactory: MDCTabScrollerFactory): MDCTabScroller | null { + const tabScrollerElement = this.root_.querySelector(strings.TAB_SCROLLER_SELECTOR); + if (tabScrollerElement) { + return tabScrollerFactory(tabScrollerElement); + } + return null; + } +} diff --git a/packages/mdc-tab-bar/foundation.ts b/packages/mdc-tab-bar/foundation.ts index 624b87c53b6..9ac666e37c2 100644 --- a/packages/mdc-tab-bar/foundation.ts +++ b/packages/mdc-tab-bar/foundation.ts @@ -22,7 +22,7 @@ */ import {MDCFoundation} from '@material/base/foundation'; -import {MDCTabDimensions, TabInteractionEventDetail} from '@material/tab/types'; +import {MDCTabDimensions, MDCTabInteractionEvent} from '@material/tab/types'; import {MDCTabBarAdapter} from './adapter'; import {numbers, strings} from './constants'; @@ -44,7 +44,7 @@ KEYCODE_MAP.set(numbers.HOME_KEYCODE, strings.HOME_KEY); KEYCODE_MAP.set(numbers.ENTER_KEYCODE, strings.ENTER_KEY); KEYCODE_MAP.set(numbers.SPACE_KEYCODE, strings.SPACE_KEY); -class MDCTabBarFoundation extends MDCFoundation { +export class MDCTabBarFoundation extends MDCFoundation { static get strings() { return strings; } @@ -141,7 +141,7 @@ class MDCTabBarFoundation extends MDCFoundation { /** * Handles the MDCTab:interacted event */ - handleTabInteraction(evt: CustomEvent) { + handleTabInteraction(evt: MDCTabInteractionEvent) { this.adapter_.setActiveTab(this.adapter_.getIndexOfTabById(evt.detail.tabId)); } @@ -403,7 +403,7 @@ class MDCTabBarFoundation extends MDCFoundation { const tabDimensions = this.adapter_.getTabDimensionsAtIndex(index); const scrollWidth = this.adapter_.getScrollContentWidth(); const nextIndex = this.findAdjacentTabIndexClosestToEdgeRTL_( - index, tabDimensions, scrollPosition, barWidth, scrollWidth); + index, tabDimensions, scrollPosition, barWidth, scrollWidth); if (!this.indexIsInRange_(nextIndex)) { return; @@ -414,4 +414,4 @@ class MDCTabBarFoundation extends MDCFoundation { } } -export {MDCTabBarFoundation as default, MDCTabBarFoundation}; +export default MDCTabBarFoundation; diff --git a/packages/mdc-tab-bar/index.ts b/packages/mdc-tab-bar/index.ts index c3c45b211e5..37eb9683f7f 100644 --- a/packages/mdc-tab-bar/index.ts +++ b/packages/mdc-tab-bar/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2018 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,155 +21,7 @@ * THE SOFTWARE. */ -import {MDCComponent} from '@material/base/component'; -import {CustomEventListener, SpecificEventListener} from '@material/base/types'; -import {MDCTabScroller} from '@material/tab-scroller/index'; -import {MDCTab, MDCTabFoundation, TabInteractionEvent} from '@material/tab/index'; -import {MDCTabBarFoundation} from './foundation'; -import {TabFactory, TabScrollerFactory} from './types'; - -let tabIdCounter = 0; - -class MDCTabBar extends MDCComponent { - static attachTo(root: Element): MDCTabBar { - return new MDCTabBar(root); - } - - private tabList_!: MDCTab[]; // assigned in initialize() - private tabScroller_!: MDCTabScroller | null; // assigned in initialize() - private handleTabInteraction_!: CustomEventListener; // assigned in initialSyncWithDOM() - private handleKeyDown_!: SpecificEventListener<'keydown'>; // assigned in initialSyncWithDOM() - - set focusOnActivate(focusOnActivate: boolean) { - this.tabList_.forEach((tab) => tab.focusOnActivate = focusOnActivate); - } - - set useAutomaticActivation(useAutomaticActivation: boolean) { - this.foundation_.setUseAutomaticActivation(useAutomaticActivation); - } - - initialize( - tabFactory: TabFactory = (el) => new MDCTab(el), - tabScrollerFactory: TabScrollerFactory = (el) => new MDCTabScroller(el), - ) { - this.tabList_ = this.instantiateTabs_(tabFactory); - this.tabScroller_ = this.instantiateTabScroller_(tabScrollerFactory); - } - - initialSyncWithDOM() { - this.handleTabInteraction_ = (evt) => this.foundation_.handleTabInteraction(evt); - this.handleKeyDown_ = (evt) => this.foundation_.handleKeyDown(evt); - - this.listen(MDCTabFoundation.strings.INTERACTED_EVENT, this.handleTabInteraction_); - this.listen('keydown', this.handleKeyDown_); - - for (let i = 0; i < this.tabList_.length; i++) { - if (this.tabList_[i].active) { - this.scrollIntoView(i); - break; - } - } - } - - destroy() { - super.destroy(); - this.unlisten(MDCTabFoundation.strings.INTERACTED_EVENT, this.handleTabInteraction_); - this.unlisten('keydown', this.handleKeyDown_); - this.tabList_.forEach((tab) => tab.destroy()); - - if (this.tabScroller_) { - this.tabScroller_.destroy(); - } - } - - getDefaultFoundation(): MDCTabBarFoundation { - // tslint:disable:object-literal-sort-keys - return new MDCTabBarFoundation({ - scrollTo: (scrollX) => this.tabScroller_!.scrollTo(scrollX), - incrementScroll: (scrollXIncrement) => this.tabScroller_!.incrementScroll(scrollXIncrement), - getScrollPosition: () => this.tabScroller_!.getScrollPosition(), - getScrollContentWidth: () => this.tabScroller_!.getScrollContentWidth(), - getOffsetWidth: () => (this.root_ as HTMLElement).offsetWidth, - isRTL: () => window.getComputedStyle(this.root_).getPropertyValue('direction') === 'rtl', - setActiveTab: (index) => this.foundation_.activateTab(index), - activateTabAtIndex: (index, clientRect) => this.tabList_[index].activate(clientRect), - deactivateTabAtIndex: (index) => this.tabList_[index].deactivate(), - focusTabAtIndex: (index) => this.tabList_[index].focus(), - getTabIndicatorClientRectAtIndex: (index) => this.tabList_[index].computeIndicatorClientRect(), - getTabDimensionsAtIndex: (index) => this.tabList_[index].computeDimensions(), - getPreviousActiveTabIndex: () => { - for (let i = 0; i < this.tabList_.length; i++) { - if (this.tabList_[i].active) { - return i; - } - } - return -1; - }, - getFocusedTabIndex: () => { - const tabElements = this.getTabElements_(); - const activeElement = document.activeElement!; - return tabElements.indexOf(activeElement); - }, - getIndexOfTabById: (id) => { - for (let i = 0; i < this.tabList_.length; i++) { - if (this.tabList_[i].id === id) { - return i; - } - } - return -1; - }, - getTabListLength: () => this.tabList_.length, - notifyTabActivated: (index) => this.emit(MDCTabBarFoundation.strings.TAB_ACTIVATED_EVENT, {index}, true), - }); - // tslint:enable:object-literal-sort-keys - } - - /** - * Activates the tab at the given index - * @param index The index of the tab - */ - activateTab(index: number) { - this.foundation_.activateTab(index); - } - - /** - * Scrolls the tab at the given index into view - * @param index THe index of the tab - */ - scrollIntoView(index: number) { - this.foundation_.scrollIntoView(index); - } - - /** - * Returns all the tab elements in a nice clean array - */ - private getTabElements_(): Element[] { - return [].slice.call(this.root_.querySelectorAll(MDCTabBarFoundation.strings.TAB_SELECTOR)); - } - - /** - * Instantiates tab components on all child tab elements - */ - private instantiateTabs_(tabFactory: TabFactory) { - return this.getTabElements_().map((el) => { - el.id = el.id || `mdc-tab-${++tabIdCounter}`; - return tabFactory(el); - }); - } - - /** - * Instantiates tab scroller component on the child tab scroller element - */ - private instantiateTabScroller_(tabScrollerFactory: TabScrollerFactory): MDCTabScroller | null { - const tabScrollerElement = this.root_.querySelector(MDCTabBarFoundation.strings.TAB_SCROLLER_SELECTOR); - if (tabScrollerElement) { - return tabScrollerFactory(tabScrollerElement); - } - return null; - } -} - -export {MDCTabBar as default, MDCTabBar}; export * from './adapter'; +export * from './component'; export * from './foundation'; export * from './types'; diff --git a/packages/mdc-tab-bar/types.ts b/packages/mdc-tab-bar/types.ts index f36458e9728..59f75121361 100644 --- a/packages/mdc-tab-bar/types.ts +++ b/packages/mdc-tab-bar/types.ts @@ -21,8 +21,12 @@ * THE SOFTWARE. */ -import {MDCTabScroller} from '@material/tab-scroller/index'; -import {MDCTab} from '@material/tab/index'; +export interface MDCTabBarActivatedEventDetail { + index: number; +} -export type TabFactory = (el: Element) => MDCTab; -export type TabScrollerFactory = (el: Element) => MDCTabScroller; +// Note: CustomEvent is not supported by Closure Compiler. + +export interface MDCTabBarActivatedEvent extends Event { + readonly detail: MDCTabBarActivatedEventDetail; +} diff --git a/packages/mdc-tab-indicator/adapter.ts b/packages/mdc-tab-indicator/adapter.ts index 236a993b67f..631ac00263e 100644 --- a/packages/mdc-tab-indicator/adapter.ts +++ b/packages/mdc-tab-indicator/adapter.ts @@ -28,7 +28,7 @@ * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCTabIndicatorAdapter { +export interface MDCTabIndicatorAdapter { /** * Adds the given className to the root element. * @param className The className to add @@ -53,5 +53,3 @@ interface MDCTabIndicatorAdapter { */ setContentStyleProperty(propName: string, value: string): void; } - -export {MDCTabIndicatorAdapter as default, MDCTabIndicatorAdapter}; diff --git a/packages/mdc-tab-indicator/component.ts b/packages/mdc-tab-indicator/component.ts new file mode 100644 index 00000000000..5cbb21993de --- /dev/null +++ b/packages/mdc-tab-indicator/component.ts @@ -0,0 +1,75 @@ +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; + +import {MDCTabIndicatorAdapter} from './adapter'; +import {MDCFadingTabIndicatorFoundation} from './fading-foundation'; +import {MDCTabIndicatorFoundation} from './foundation'; +import {MDCSlidingTabIndicatorFoundation} from './sliding-foundation'; + +export type MDCTabIndicatorFactory = (el: Element, foundation?: MDCTabIndicatorFoundation) => MDCTabIndicator; + +export class MDCTabIndicator extends MDCComponent { + static attachTo(root: Element): MDCTabIndicator { + return new MDCTabIndicator(root); + } + + private content_!: HTMLElement; // assigned in initialize() + + initialize() { + this.content_ = this.root_.querySelector(MDCTabIndicatorFoundation.strings.CONTENT_SELECTOR)!; + } + + computeContentClientRect(): ClientRect { + return this.foundation_.computeContentClientRect(); + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + // tslint:disable:object-literal-sort-keys + const adapter: MDCTabIndicatorAdapter = { + addClass: (className) => this.root_.classList.add(className), + removeClass: (className) => this.root_.classList.remove(className), + computeContentClientRect: () => this.content_.getBoundingClientRect(), + setContentStyleProperty: (prop, value) => this.content_.style.setProperty(prop, value), + }; + // tslint:enable:object-literal-sort-keys + + if (this.root_.classList.contains(MDCTabIndicatorFoundation.cssClasses.FADE)) { + return new MDCFadingTabIndicatorFoundation(adapter); + } + + // Default to the sliding indicator + return new MDCSlidingTabIndicatorFoundation(adapter); + } + + activate(previousIndicatorClientRect?: ClientRect) { + this.foundation_.activate(previousIndicatorClientRect); + } + + deactivate() { + this.foundation_.deactivate(); + } +} diff --git a/packages/mdc-tab-indicator/fading-foundation.ts b/packages/mdc-tab-indicator/fading-foundation.ts index 2be01e9790b..06ee345f441 100644 --- a/packages/mdc-tab-indicator/fading-foundation.ts +++ b/packages/mdc-tab-indicator/fading-foundation.ts @@ -24,7 +24,7 @@ import {MDCTabIndicatorFoundation} from './foundation'; /* istanbul ignore next: subclass is not a branch statement */ -class MDCFadingTabIndicatorFoundation extends MDCTabIndicatorFoundation { +export class MDCFadingTabIndicatorFoundation extends MDCTabIndicatorFoundation { activate() { this.adapter_.addClass(MDCTabIndicatorFoundation.cssClasses.ACTIVE); } @@ -34,4 +34,4 @@ class MDCFadingTabIndicatorFoundation extends MDCTabIndicatorFoundation { } } -export {MDCFadingTabIndicatorFoundation as default, MDCFadingTabIndicatorFoundation}; +export default MDCFadingTabIndicatorFoundation; diff --git a/packages/mdc-tab-indicator/foundation.ts b/packages/mdc-tab-indicator/foundation.ts index bab499e44bc..35194cb6ab7 100644 --- a/packages/mdc-tab-indicator/foundation.ts +++ b/packages/mdc-tab-indicator/foundation.ts @@ -25,7 +25,7 @@ import {MDCFoundation} from '@material/base/foundation'; import {MDCTabIndicatorAdapter} from './adapter'; import {cssClasses, strings} from './constants'; -abstract class MDCTabIndicatorFoundation extends MDCFoundation { +export abstract class MDCTabIndicatorFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } @@ -57,4 +57,4 @@ abstract class MDCTabIndicatorFoundation extends MDCFoundation { - static attachTo(root: Element): MDCTabIndicator { - return new MDCTabIndicator(root); - } - - private content_!: HTMLElement; // assigned in initialize() - - initialize() { - this.content_ = this.root_.querySelector(MDCTabIndicatorFoundation.strings.CONTENT_SELECTOR)!; - } - - computeContentClientRect(): ClientRect { - return this.foundation_.computeContentClientRect(); - } - - getDefaultFoundation(): MDCTabIndicatorFoundation { - // tslint:disable:object-literal-sort-keys - const adapter: MDCTabIndicatorAdapter = { - addClass: (className) => this.root_.classList.add(className), - removeClass: (className) => this.root_.classList.remove(className), - computeContentClientRect: () => this.content_.getBoundingClientRect(), - setContentStyleProperty: (prop, value) => this.content_.style.setProperty(prop, value), - }; - // tslint:enable:object-literal-sort-keys - - if (this.root_.classList.contains(MDCTabIndicatorFoundation.cssClasses.FADE)) { - return new MDCFadingTabIndicatorFoundation(adapter); - } - - // Default to the sliding indicator - return new MDCSlidingTabIndicatorFoundation(adapter); - } - - activate(previousIndicatorClientRect?: ClientRect) { - this.foundation_.activate(previousIndicatorClientRect); - } - - deactivate() { - this.foundation_.deactivate(); - } -} - -export {MDCTabIndicator as default, MDCTabIndicator}; export * from './adapter'; +export * from './component'; export * from './foundation'; export * from './fading-foundation'; export * from './sliding-foundation'; diff --git a/packages/mdc-tab-indicator/sliding-foundation.ts b/packages/mdc-tab-indicator/sliding-foundation.ts index 943c57bf7ca..7426d94f350 100644 --- a/packages/mdc-tab-indicator/sliding-foundation.ts +++ b/packages/mdc-tab-indicator/sliding-foundation.ts @@ -24,7 +24,7 @@ import {MDCTabIndicatorFoundation} from './foundation'; /* istanbul ignore next: subclass is not a branch statement */ -class MDCSlidingTabIndicatorFoundation extends MDCTabIndicatorFoundation { +export class MDCSlidingTabIndicatorFoundation extends MDCTabIndicatorFoundation { activate(previousIndicatorClientRect?: ClientRect) { // Early exit if no indicator is present to handle cases where an indicator // may be activated without a prior indicator state @@ -56,4 +56,4 @@ class MDCSlidingTabIndicatorFoundation extends MDCTabIndicatorFoundation { } } -export {MDCSlidingTabIndicatorFoundation as default, MDCSlidingTabIndicatorFoundation}; +export default MDCSlidingTabIndicatorFoundation; diff --git a/packages/mdc-tab-scroller/adapter.ts b/packages/mdc-tab-scroller/adapter.ts index 430ef725345..07a20d5218f 100644 --- a/packages/mdc-tab-scroller/adapter.ts +++ b/packages/mdc-tab-scroller/adapter.ts @@ -28,7 +28,7 @@ * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCTabScrollerAdapter { +export interface MDCTabScrollerAdapter { /** * Adds the given className to the root element. * @param className The className to add @@ -110,5 +110,3 @@ interface MDCTabScrollerAdapter { */ computeHorizontalScrollbarHeight(): number; } - -export {MDCTabScrollerAdapter as default, MDCTabScrollerAdapter}; diff --git a/packages/mdc-tab-scroller/component.ts b/packages/mdc-tab-scroller/component.ts new file mode 100644 index 00000000000..4dbed29003e --- /dev/null +++ b/packages/mdc-tab-scroller/component.ts @@ -0,0 +1,126 @@ +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {SpecificEventListener} from '@material/base/types'; +import {ponyfill} from '@material/dom/index'; +import {MDCTabScrollerAdapter} from './adapter'; +import {MDCTabScrollerFoundation} from './foundation'; +import * as util from './util'; + +type InteractionEventType = 'wheel' | 'touchstart' | 'pointerdown' | 'mousedown' | 'keydown'; + +export type MDCTabScrollerFactory = (el: Element, foundation?: MDCTabScrollerFoundation) => MDCTabScroller; + +export class MDCTabScroller extends MDCComponent { + static attachTo(root: Element): MDCTabScroller { + return new MDCTabScroller(root); + } + + private content_!: HTMLElement; // assigned in initialize() + private area_!: HTMLElement; // assigned in initialize() + private handleInteraction_!: SpecificEventListener; // assigned in initialSyncWithDOM() + private handleTransitionEnd_!: SpecificEventListener<'transitionend'>; // assigned in initialSyncWithDOM() + + initialize() { + this.area_ = this.root_.querySelector(MDCTabScrollerFoundation.strings.AREA_SELECTOR)!; + this.content_ = this.root_.querySelector(MDCTabScrollerFoundation.strings.CONTENT_SELECTOR)!; + } + + initialSyncWithDOM() { + this.handleInteraction_ = () => this.foundation_.handleInteraction(); + this.handleTransitionEnd_ = (evt) => this.foundation_.handleTransitionEnd(evt); + + this.area_.addEventListener('wheel', this.handleInteraction_); + this.area_.addEventListener('touchstart', this.handleInteraction_); + this.area_.addEventListener('pointerdown', this.handleInteraction_); + this.area_.addEventListener('mousedown', this.handleInteraction_); + this.area_.addEventListener('keydown', this.handleInteraction_); + this.content_.addEventListener('transitionend', this.handleTransitionEnd_); + } + + destroy() { + super.destroy(); + + this.area_.removeEventListener('wheel', this.handleInteraction_); + this.area_.removeEventListener('touchstart', this.handleInteraction_); + this.area_.removeEventListener('pointerdown', this.handleInteraction_); + this.area_.removeEventListener('mousedown', this.handleInteraction_); + this.area_.removeEventListener('keydown', this.handleInteraction_); + this.content_.removeEventListener('transitionend', this.handleTransitionEnd_); + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + // tslint:disable:object-literal-sort-keys + const adapter: MDCTabScrollerAdapter = { + eventTargetMatchesSelector: (evtTarget, selector) => ponyfill.matches(evtTarget as Element, selector), + addClass: (className) => this.root_.classList.add(className), + removeClass: (className) => this.root_.classList.remove(className), + addScrollAreaClass: (className) => this.area_.classList.add(className), + setScrollAreaStyleProperty: (prop, value) => this.area_.style.setProperty(prop, value), + setScrollContentStyleProperty: (prop, value) => this.content_.style.setProperty(prop, value), + getScrollContentStyleValue: (propName) => window.getComputedStyle(this.content_).getPropertyValue(propName), + setScrollAreaScrollLeft: (scrollX) => this.area_.scrollLeft = scrollX, + getScrollAreaScrollLeft: () => this.area_.scrollLeft, + getScrollContentOffsetWidth: () => this.content_.offsetWidth, + getScrollAreaOffsetWidth: () => this.area_.offsetWidth, + computeScrollAreaClientRect: () => this.area_.getBoundingClientRect(), + computeScrollContentClientRect: () => this.content_.getBoundingClientRect(), + computeHorizontalScrollbarHeight: () => util.computeHorizontalScrollbarHeight(document), + }; + // tslint:enable:object-literal-sort-keys + return new MDCTabScrollerFoundation(adapter); + } + + /** + * Returns the current visual scroll position + */ + getScrollPosition(): number { + return this.foundation_.getScrollPosition(); + } + + /** + * Returns the width of the scroll content + */ + getScrollContentWidth(): number { + return this.content_.offsetWidth; + } + + /** + * Increments the scroll value by the given amount + * @param scrollXIncrement The pixel value by which to increment the scroll value + */ + incrementScroll(scrollXIncrement: number) { + this.foundation_.incrementScroll(scrollXIncrement); + } + + /** + * Scrolls to the given pixel position + * @param scrollX The pixel value to scroll to + */ + scrollTo(scrollX: number) { + this.foundation_.scrollTo(scrollX); + } +} diff --git a/packages/mdc-tab-scroller/foundation.ts b/packages/mdc-tab-scroller/foundation.ts index 1a52fd9720b..7904cd910fb 100644 --- a/packages/mdc-tab-scroller/foundation.ts +++ b/packages/mdc-tab-scroller/foundation.ts @@ -30,7 +30,7 @@ import {MDCTabScrollerRTLReverse} from './rtl-reverse-scroller'; import {MDCTabScrollerRTL} from './rtl-scroller'; import {MDCTabScrollerAnimation, MDCTabScrollerHorizontalEdges} from './types'; -class MDCTabScrollerFoundation extends MDCFoundation { +export class MDCTabScrollerFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } @@ -177,7 +177,12 @@ class MDCTabScrollerFoundation extends MDCFoundation { // of `matrix(a, b, c, d, tx, ty)`. We only care about tx (translateX) so // we're going to grab all the parenthesized values, strip out tx, and // parse it. - const [, matrixParams] = /\((.+?)\)/.exec(transformValue); + const match = /\((.+?)\)/.exec(transformValue); + if (!match) { + return 0; + } + + const matrixParams = match[1]; // @ts-ignore const [a, b, c, d, tx, ty] = matrixParams.split(','); @@ -361,4 +366,4 @@ class MDCTabScrollerFoundation extends MDCFoundation { } } -export {MDCTabScrollerFoundation as default, MDCTabScrollerFoundation}; +export default MDCTabScrollerFoundation; diff --git a/packages/mdc-tab-scroller/index.ts b/packages/mdc-tab-scroller/index.ts index 4d926eb2c99..3de836c29e1 100644 --- a/packages/mdc-tab-scroller/index.ts +++ b/packages/mdc-tab-scroller/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2018 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,107 +21,10 @@ * THE SOFTWARE. */ -import {MDCComponent} from '@material/base/component'; -import {SpecificEventListener} from '@material/base/types'; -import {ponyfill} from '@material/dom/index'; -import {MDCTabScrollerAdapter} from './adapter'; -import {MDCTabScrollerFoundation} from './foundation'; import * as util from './util'; -type InteractionEventType = 'wheel' | 'touchstart' | 'pointerdown' | 'mousedown' | 'keydown'; - -class MDCTabScroller extends MDCComponent { - static attachTo(root: Element): MDCTabScroller { - return new MDCTabScroller(root); - } - - private content_!: HTMLElement; // assigned in initialize() - private area_!: HTMLElement; // assigned in initialize() - private handleInteraction_!: SpecificEventListener; // assigned in initialSyncWithDOM() - private handleTransitionEnd_!: SpecificEventListener<'transitionend'>; // assigned in initialSyncWithDOM() - - initialize() { - this.area_ = this.root_.querySelector(MDCTabScrollerFoundation.strings.AREA_SELECTOR)!; - this.content_ = this.root_.querySelector(MDCTabScrollerFoundation.strings.CONTENT_SELECTOR)!; - } - - initialSyncWithDOM() { - this.handleInteraction_ = () => this.foundation_.handleInteraction(); - this.handleTransitionEnd_ = (evt) => this.foundation_.handleTransitionEnd(evt); - - this.area_.addEventListener('wheel', this.handleInteraction_); - this.area_.addEventListener('touchstart', this.handleInteraction_); - this.area_.addEventListener('pointerdown', this.handleInteraction_); - this.area_.addEventListener('mousedown', this.handleInteraction_); - this.area_.addEventListener('keydown', this.handleInteraction_); - this.content_.addEventListener('transitionend', this.handleTransitionEnd_); - } - - destroy() { - super.destroy(); - - this.area_.removeEventListener('wheel', this.handleInteraction_); - this.area_.removeEventListener('touchstart', this.handleInteraction_); - this.area_.removeEventListener('pointerdown', this.handleInteraction_); - this.area_.removeEventListener('mousedown', this.handleInteraction_); - this.area_.removeEventListener('keydown', this.handleInteraction_); - this.content_.removeEventListener('transitionend', this.handleTransitionEnd_); - } - - getDefaultFoundation(): MDCTabScrollerFoundation { - // tslint:disable:object-literal-sort-keys - const adapter: MDCTabScrollerAdapter = { - eventTargetMatchesSelector: (evtTarget, selector) => ponyfill.matches(evtTarget as Element, selector), - addClass: (className) => this.root_.classList.add(className), - removeClass: (className) => this.root_.classList.remove(className), - addScrollAreaClass: (className) => this.area_.classList.add(className), - setScrollAreaStyleProperty: (prop, value) => this.area_.style.setProperty(prop, value), - setScrollContentStyleProperty: (prop, value) => this.content_.style.setProperty(prop, value), - getScrollContentStyleValue: (propName) => window.getComputedStyle(this.content_).getPropertyValue(propName), - setScrollAreaScrollLeft: (scrollX) => this.area_.scrollLeft = scrollX, - getScrollAreaScrollLeft: () => this.area_.scrollLeft, - getScrollContentOffsetWidth: () => this.content_.offsetWidth, - getScrollAreaOffsetWidth: () => this.area_.offsetWidth, - computeScrollAreaClientRect: () => this.area_.getBoundingClientRect(), - computeScrollContentClientRect: () => this.content_.getBoundingClientRect(), - computeHorizontalScrollbarHeight: () => util.computeHorizontalScrollbarHeight(document), - }; - // tslint:enable:object-literal-sort-keys - return new MDCTabScrollerFoundation(adapter); - } - - /** - * Returns the current visual scroll position - */ - getScrollPosition(): number { - return this.foundation_.getScrollPosition(); - } - - /** - * Returns the width of the scroll content - */ - getScrollContentWidth(): number { - return this.content_.offsetWidth; - } - - /** - * Increments the scroll value by the given amount - * @param scrollXIncrement The pixel value by which to increment the scroll value - */ - incrementScroll(scrollXIncrement: number) { - this.foundation_.incrementScroll(scrollXIncrement); - } - - /** - * Scrolls to the given pixel position - * @param scrollX The pixel value to scroll to - */ - scrollTo(scrollX: number) { - this.foundation_.scrollTo(scrollX); - } -} - -export {MDCTabScroller as default, MDCTabScroller, util}; +export {util}; export * from './adapter'; +export * from './component'; export * from './foundation'; export * from './types'; diff --git a/packages/mdc-tab-scroller/rtl-default-scroller.ts b/packages/mdc-tab-scroller/rtl-default-scroller.ts index 083a6b5676a..2f8e58ca27c 100644 --- a/packages/mdc-tab-scroller/rtl-default-scroller.ts +++ b/packages/mdc-tab-scroller/rtl-default-scroller.ts @@ -24,7 +24,7 @@ import {MDCTabScrollerRTL} from './rtl-scroller'; import {MDCTabScrollerAnimation, MDCTabScrollerHorizontalEdges} from './types'; -class MDCTabScrollerRTLDefault extends MDCTabScrollerRTL { +export class MDCTabScrollerRTLDefault extends MDCTabScrollerRTL { getScrollPositionRTL(): number { const currentScrollLeft = this.adapter_.getScrollAreaScrollLeft(); const {right} = this.calculateScrollEdges_(); @@ -70,4 +70,4 @@ class MDCTabScrollerRTLDefault extends MDCTabScrollerRTL { } } -export {MDCTabScrollerRTLDefault as default, MDCTabScrollerRTLDefault}; +export default MDCTabScrollerRTLDefault; // For backward compatibility diff --git a/packages/mdc-tab-scroller/rtl-negative-scroller.ts b/packages/mdc-tab-scroller/rtl-negative-scroller.ts index afb7f5e38ff..05181eefacf 100644 --- a/packages/mdc-tab-scroller/rtl-negative-scroller.ts +++ b/packages/mdc-tab-scroller/rtl-negative-scroller.ts @@ -24,7 +24,7 @@ import {MDCTabScrollerRTL} from './rtl-scroller'; import {MDCTabScrollerAnimation, MDCTabScrollerHorizontalEdges} from './types'; -class MDCTabScrollerRTLNegative extends MDCTabScrollerRTL { +export class MDCTabScrollerRTLNegative extends MDCTabScrollerRTL { getScrollPositionRTL(translateX: number): number { const currentScrollLeft = this.adapter_.getScrollAreaScrollLeft(); return Math.round(translateX - currentScrollLeft); @@ -67,4 +67,4 @@ class MDCTabScrollerRTLNegative extends MDCTabScrollerRTL { } } -export {MDCTabScrollerRTLNegative as default, MDCTabScrollerRTLNegative}; +export default MDCTabScrollerRTLNegative; // For backward compatibility diff --git a/packages/mdc-tab-scroller/rtl-reverse-scroller.ts b/packages/mdc-tab-scroller/rtl-reverse-scroller.ts index 83748615cf6..d6eeaf67327 100644 --- a/packages/mdc-tab-scroller/rtl-reverse-scroller.ts +++ b/packages/mdc-tab-scroller/rtl-reverse-scroller.ts @@ -24,7 +24,7 @@ import {MDCTabScrollerRTL} from './rtl-scroller'; import {MDCTabScrollerAnimation, MDCTabScrollerHorizontalEdges} from './types'; -class MDCTabScrollerRTLReverse extends MDCTabScrollerRTL { +export class MDCTabScrollerRTLReverse extends MDCTabScrollerRTL { getScrollPositionRTL(translateX: number): number { const currentScrollLeft = this.adapter_.getScrollAreaScrollLeft(); // Scroll values on most browsers are ints instead of floats so we round @@ -68,4 +68,4 @@ class MDCTabScrollerRTLReverse extends MDCTabScrollerRTL { } } -export {MDCTabScrollerRTLReverse as default, MDCTabScrollerRTLReverse}; +export default MDCTabScrollerRTLReverse; // For backward compatibility diff --git a/packages/mdc-tab-scroller/rtl-scroller.ts b/packages/mdc-tab-scroller/rtl-scroller.ts index 6453715de8e..696b6e4a245 100644 --- a/packages/mdc-tab-scroller/rtl-scroller.ts +++ b/packages/mdc-tab-scroller/rtl-scroller.ts @@ -24,7 +24,7 @@ import {MDCTabScrollerAdapter} from './adapter'; import {MDCTabScrollerAnimation} from './types'; -abstract class MDCTabScrollerRTL { +export abstract class MDCTabScrollerRTL { protected readonly adapter_: MDCTabScrollerAdapter; constructor(adapter: MDCTabScrollerAdapter) { @@ -44,4 +44,4 @@ abstract class MDCTabScrollerRTL { abstract getAnimatingScrollPosition(scrollX: number, translateX: number): number; } -export {MDCTabScrollerRTL as default, MDCTabScrollerRTL}; +export default MDCTabScrollerRTL; // For backward compatibility diff --git a/packages/mdc-tab/adapter.ts b/packages/mdc-tab/adapter.ts index 036686136e6..1af050df22b 100644 --- a/packages/mdc-tab/adapter.ts +++ b/packages/mdc-tab/adapter.ts @@ -28,7 +28,7 @@ * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCTabAdapter { +export interface MDCTabAdapter { /** * Adds the given className to the root element. * @param className The className to add @@ -93,5 +93,3 @@ interface MDCTabAdapter { */ focus(): void; } - -export {MDCTabAdapter as default, MDCTabAdapter}; diff --git a/packages/mdc-tab/component.ts b/packages/mdc-tab/component.ts new file mode 100644 index 00000000000..14089a1b717 --- /dev/null +++ b/packages/mdc-tab/component.ts @@ -0,0 +1,145 @@ +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {SpecificEventListener} from '@material/base/types'; +import {MDCRipple, MDCRippleCapableSurface, MDCRippleFactory, MDCRippleFoundation} from '@material/ripple/index'; +import {MDCTabIndicator, MDCTabIndicatorFactory} from '@material/tab-indicator/index'; +import {MDCTabAdapter} from './adapter'; +import {MDCTabFoundation} from './foundation'; +import {MDCTabDimensions, MDCTabInteractionEventDetail} from './types'; + +export type MDCTabFactory = (el: Element, foundation?: MDCTabFoundation) => MDCTab; + +export class MDCTab extends MDCComponent implements MDCRippleCapableSurface { + static attachTo(root: Element): MDCTab { + return new MDCTab(root); + } + + id!: string; // assigned in initialize(); + + // Public visibility for this property is required by MDCRippleCapableSurface. + root_!: HTMLElement; // assigned in MDCComponent constructor + + private ripple_!: MDCRipple; // assigned in initialize(); + private tabIndicator_!: MDCTabIndicator; // assigned in initialize(); + private content_!: HTMLElement; // assigned in initialize(); + private handleClick_!: SpecificEventListener<'click'>; // assigned in initialize(); + + initialize( + rippleFactory: MDCRippleFactory = (el, foundation) => new MDCRipple(el, foundation), + tabIndicatorFactory: MDCTabIndicatorFactory = (el) => new MDCTabIndicator(el), + ) { + this.id = this.root_.id; + const rippleSurface = this.root_.querySelector(MDCTabFoundation.strings.RIPPLE_SELECTOR)!; + const rippleAdapter = { + ...MDCRipple.createAdapter(this), + addClass: (className: string) => rippleSurface.classList.add(className), + removeClass: (className: string) => rippleSurface.classList.remove(className), + updateCssVariable: (varName: string, value: string) => rippleSurface.style.setProperty(varName, value), + }; + const rippleFoundation = new MDCRippleFoundation(rippleAdapter); + this.ripple_ = rippleFactory(this.root_, rippleFoundation); + + const tabIndicatorElement = this.root_.querySelector(MDCTabFoundation.strings.TAB_INDICATOR_SELECTOR)!; + this.tabIndicator_ = tabIndicatorFactory(tabIndicatorElement); + this.content_ = this.root_.querySelector(MDCTabFoundation.strings.CONTENT_SELECTOR)!; + } + + initialSyncWithDOM() { + this.handleClick_ = () => this.foundation_.handleClick(); + this.listen('click', this.handleClick_); + } + + destroy() { + this.unlisten('click', this.handleClick_); + this.ripple_.destroy(); + super.destroy(); + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + // tslint:disable:object-literal-sort-keys + const adapter: MDCTabAdapter = { + setAttr: (attr, value) => this.root_.setAttribute(attr, value), + addClass: (className) => this.root_.classList.add(className), + removeClass: (className) => this.root_.classList.remove(className), + hasClass: (className) => this.root_.classList.contains(className), + activateIndicator: (previousIndicatorClientRect) => this.tabIndicator_.activate(previousIndicatorClientRect), + deactivateIndicator: () => this.tabIndicator_.deactivate(), + notifyInteracted: () => this.emit( + MDCTabFoundation.strings.INTERACTED_EVENT, {tabId: this.id}, true /* bubble */), + getOffsetLeft: () => this.root_.offsetLeft, + getOffsetWidth: () => this.root_.offsetWidth, + getContentOffsetLeft: () => this.content_.offsetLeft, + getContentOffsetWidth: () => this.content_.offsetWidth, + focus: () => this.root_.focus(), + }; + // tslint:enable:object-literal-sort-keys + return new MDCTabFoundation(adapter); + } + + /** + * Getter for the active state of the tab + */ + get active(): boolean { + return this.foundation_.isActive(); + } + + set focusOnActivate(focusOnActivate: boolean) { + this.foundation_.setFocusOnActivate(focusOnActivate); + } + + /** + * Activates the tab + */ + activate(computeIndicatorClientRect?: ClientRect) { + this.foundation_.activate(computeIndicatorClientRect); + } + + /** + * Deactivates the tab + */ + deactivate() { + this.foundation_.deactivate(); + } + + /** + * Returns the indicator's client rect + */ + computeIndicatorClientRect(): ClientRect { + return this.tabIndicator_.computeContentClientRect(); + } + + computeDimensions(): MDCTabDimensions { + return this.foundation_.computeDimensions(); + } + + /** + * Focuses the tab + */ + focus() { + this.root_.focus(); + } +} diff --git a/packages/mdc-tab/foundation.ts b/packages/mdc-tab/foundation.ts index cb3f076d3b6..2cb36545ef4 100644 --- a/packages/mdc-tab/foundation.ts +++ b/packages/mdc-tab/foundation.ts @@ -26,7 +26,7 @@ import {MDCTabAdapter} from './adapter'; import {cssClasses, strings} from './constants'; import {MDCTabDimensions} from './types'; -class MDCTabFoundation extends MDCFoundation { +export class MDCTabFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } @@ -123,4 +123,4 @@ class MDCTabFoundation extends MDCFoundation { } } -export {MDCTabFoundation as default, MDCTabFoundation}; +export default MDCTabFoundation; diff --git a/packages/mdc-tab/index.ts b/packages/mdc-tab/index.ts index c33ba92f1b5..37eb9683f7f 100644 --- a/packages/mdc-tab/index.ts +++ b/packages/mdc-tab/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2018 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,124 +21,7 @@ * THE SOFTWARE. */ -import {MDCComponent} from '@material/base/component'; -import {SpecificEventListener} from '@material/base/types'; -import {MDCRipple, MDCRippleFoundation, RippleCapableSurface} from '@material/ripple/index'; -import {MDCTabIndicator} from '@material/tab-indicator/index'; -import {MDCTabFoundation} from './foundation'; -import {MDCTabDimensions, RippleFactory, TabIndicatorFactory, TabInteractionEventDetail} from './types'; - -class MDCTab extends MDCComponent implements RippleCapableSurface { - static attachTo(root: Element): MDCTab { - return new MDCTab(root); - } - - id!: string; // assigned in initialize(); - - // Public visibility for this property is required by RippleCapableSurface. - root_!: HTMLElement; // assigned in MDCComponent constructor - - private ripple_!: MDCRipple; // assigned in initialize(); - private tabIndicator_!: MDCTabIndicator; // assigned in initialize(); - private content_!: HTMLElement; // assigned in initialize(); - private handleClick_!: SpecificEventListener<'click'>; // assigned in initialize(); - - initialize( - rippleFactory: RippleFactory = (el, foundation) => new MDCRipple(el, foundation), - tabIndicatorFactory: TabIndicatorFactory = (el) => new MDCTabIndicator(el), - ) { - this.id = this.root_.id; - const rippleSurface = this.root_.querySelector(MDCTabFoundation.strings.RIPPLE_SELECTOR)!; - const rippleAdapter = { - ...MDCRipple.createAdapter(this), - addClass: (className: string) => rippleSurface.classList.add(className), - removeClass: (className: string) => rippleSurface.classList.remove(className), - updateCssVariable: (varName: string, value: string) => rippleSurface.style.setProperty(varName, value), - }; - const rippleFoundation = new MDCRippleFoundation(rippleAdapter); - this.ripple_ = rippleFactory(this.root_, rippleFoundation); - - const tabIndicatorElement = this.root_.querySelector(MDCTabFoundation.strings.TAB_INDICATOR_SELECTOR)!; - this.tabIndicator_ = tabIndicatorFactory(tabIndicatorElement); - this.content_ = this.root_.querySelector(MDCTabFoundation.strings.CONTENT_SELECTOR)!; - } - - initialSyncWithDOM() { - this.handleClick_ = () => this.foundation_.handleClick(); - this.listen('click', this.handleClick_); - } - - destroy() { - this.unlisten('click', this.handleClick_); - this.ripple_.destroy(); - super.destroy(); - } - - getDefaultFoundation(): MDCTabFoundation { - // tslint:disable:object-literal-sort-keys - return new MDCTabFoundation({ - setAttr: (attr, value) => this.root_.setAttribute(attr, value), - addClass: (className) => this.root_.classList.add(className), - removeClass: (className) => this.root_.classList.remove(className), - hasClass: (className) => this.root_.classList.contains(className), - activateIndicator: (previousIndicatorClientRect) => this.tabIndicator_.activate(previousIndicatorClientRect), - deactivateIndicator: () => this.tabIndicator_.deactivate(), - notifyInteracted: () => this.emit( - MDCTabFoundation.strings.INTERACTED_EVENT, {tabId: this.id}, true /* bubble */), - getOffsetLeft: () => this.root_.offsetLeft, - getOffsetWidth: () => this.root_.offsetWidth, - getContentOffsetLeft: () => this.content_.offsetLeft, - getContentOffsetWidth: () => this.content_.offsetWidth, - focus: () => this.root_.focus(), - }); - // tslint:enable:object-literal-sort-keys - } - - /** - * Getter for the active state of the tab - */ - get active(): boolean { - return this.foundation_.isActive(); - } - - set focusOnActivate(focusOnActivate: boolean) { - this.foundation_.setFocusOnActivate(focusOnActivate); - } - - /** - * Activates the tab - */ - activate(computeIndicatorClientRect?: ClientRect) { - this.foundation_.activate(computeIndicatorClientRect); - } - - /** - * Deactivates the tab - */ - deactivate() { - this.foundation_.deactivate(); - } - - /** - * Returns the indicator's client rect - */ - computeIndicatorClientRect(): ClientRect { - return this.tabIndicator_.computeContentClientRect(); - } - - computeDimensions(): MDCTabDimensions { - return this.foundation_.computeDimensions(); - } - - /** - * Focuses the tab - */ - focus() { - this.root_.focus(); - } -} - -export {MDCTab as default, MDCTab}; export * from './adapter'; +export * from './component'; export * from './foundation'; export * from './types'; diff --git a/packages/mdc-tab/mdc-tab.scss b/packages/mdc-tab/mdc-tab.scss index caa3be79a5e..dc638be5a7b 100644 --- a/packages/mdc-tab/mdc-tab.scss +++ b/packages/mdc-tab/mdc-tab.scss @@ -46,8 +46,6 @@ outline: none; background: none; text-align: center; - text-decoration: none; - text-transform: uppercase; white-space: nowrap; cursor: pointer; -webkit-appearance: none; diff --git a/packages/mdc-tab/types.ts b/packages/mdc-tab/types.ts index 7058fa4c595..f6672be8539 100644 --- a/packages/mdc-tab/types.ts +++ b/packages/mdc-tab/types.ts @@ -21,10 +21,6 @@ * THE SOFTWARE. */ -import {MDCRippleFoundation} from '@material/ripple/foundation'; -import {MDCRipple} from '@material/ripple/index'; -import {MDCTabIndicator} from '@material/tab-indicator/index'; - /** * MDCTabDimensions provides details about the left and right edges of the Tab * root element and the Tab content element. These values are used to determine @@ -37,11 +33,12 @@ export interface MDCTabDimensions { contentRight: number; } -export type TabInteractionEvent = CustomEvent; - -export interface TabInteractionEventDetail { +export interface MDCTabInteractionEventDetail { tabId: string; } -export type RippleFactory = (el: Element, foundation: MDCRippleFoundation) => MDCRipple; -export type TabIndicatorFactory = (el: Element) => MDCTabIndicator; +// Note: CustomEvent is not supported by Closure Compiler. + +export interface MDCTabInteractionEvent extends Event { + readonly detail: MDCTabInteractionEventDetail; +} diff --git a/packages/mdc-tabs/index.ts b/packages/mdc-tabs/index.ts index 1752cfa1d60..fef26f581dc 100644 --- a/packages/mdc-tabs/index.ts +++ b/packages/mdc-tabs/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,6 +21,6 @@ * THE SOFTWARE. */ -export * from './tab'; -export * from './tab-bar'; -export * from './tab-bar-scroller'; +export * from './tab/index'; +export * from './tab-bar/index'; +export * from './tab-bar-scroller/index'; diff --git a/packages/mdc-tabs/tab-bar-scroller/adapter.ts b/packages/mdc-tabs/tab-bar-scroller/adapter.ts index 428d5301acd..4f1586d95d4 100644 --- a/packages/mdc-tabs/tab-bar-scroller/adapter.ts +++ b/packages/mdc-tabs/tab-bar-scroller/adapter.ts @@ -51,5 +51,3 @@ export interface MDCTabBarScrollerAdapter { getOffsetLeftForEventTarget: (target: HTMLElement) => number; getOffsetWidthForEventTarget: (target: HTMLElement) => number; } - -export default MDCTabBarScrollerAdapter; diff --git a/packages/mdc-tabs/tab-bar-scroller/component.ts b/packages/mdc-tabs/tab-bar-scroller/component.ts index 5ebb9719d49..db9596db469 100644 --- a/packages/mdc-tabs/tab-bar-scroller/component.ts +++ b/packages/mdc-tabs/tab-bar-scroller/component.ts @@ -21,7 +21,7 @@ * THE SOFTWARE. */ -import {getCorrectPropertyName} from '@material/animation/index'; +import {getCorrectPropertyName} from '@material/animation/util'; import {MDCComponent} from '@material/base/component'; import {MDCTabBar, MDCTabBarFactory} from '../tab-bar/index'; import {MDCTabBarScrollerAdapter} from './adapter'; @@ -46,18 +46,20 @@ export class MDCTabBarScroller extends MDCComponent initialize(tabBarFactory: MDCTabBarFactory = (el) => new MDCTabBar(el)) { this.scrollFrame_ = - this.root_.querySelector(MDCTabBarScrollerFoundation.strings.FRAME_SELECTOR)!; + this.root_.querySelector(MDCTabBarScrollerFoundation.strings.FRAME_SELECTOR)!; this.tabBarEl_ = - this.root_.querySelector(MDCTabBarScrollerFoundation.strings.TABS_SELECTOR)!; + this.root_.querySelector(MDCTabBarScrollerFoundation.strings.TABS_SELECTOR)!; this.forwardIndicator_ = - this.root_.querySelector(MDCTabBarScrollerFoundation.strings.INDICATOR_FORWARD_SELECTOR)!; + this.root_.querySelector(MDCTabBarScrollerFoundation.strings.INDICATOR_FORWARD_SELECTOR)!; this.backIndicator_ = - this.root_.querySelector(MDCTabBarScrollerFoundation.strings.INDICATOR_BACK_SELECTOR)!; + this.root_.querySelector(MDCTabBarScrollerFoundation.strings.INDICATOR_BACK_SELECTOR)!; this.tabBar_ = tabBarFactory(this.tabBarEl_); } getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. // tslint:disable:object-literal-sort-keys const adapter: MDCTabBarScrollerAdapter = { addClass: (className) => this.root_.classList.add(className), diff --git a/packages/mdc-tabs/tab-bar-scroller/foundation.ts b/packages/mdc-tabs/tab-bar-scroller/foundation.ts index 9922a162a4b..beb5c9ab47a 100644 --- a/packages/mdc-tabs/tab-bar-scroller/foundation.ts +++ b/packages/mdc-tabs/tab-bar-scroller/foundation.ts @@ -26,7 +26,7 @@ import {MDCFoundation} from '@material/base/foundation'; import {MDCTabBarScrollerAdapter} from './adapter'; import {cssClasses, strings} from './constants'; -export type InteractionEventType = 'touchstart' | 'mousedown' | 'focus'; +type InteractionEventType = 'touchstart' | 'mousedown' | 'focus'; const INTERACTION_EVENTS: InteractionEventType[] = ['touchstart', 'mousedown', 'focus']; @@ -162,14 +162,14 @@ export class MDCTabBarScrollerFoundation extends MDCFoundation scrollFrameOffsetWidth; if (this.isRTL_()) { const frameOffsetAndTabWidth = - scrollFrameOffsetWidth - this.adapter_.getComputedWidthForTabAtIndex(i); + scrollFrameOffsetWidth - this.adapter_.getComputedWidthForTabAtIndex(i); const tabRightOffset = - this.adapter_.getOffsetWidthForTabBar() - tabOffsetLeftAndWidth; + this.adapter_.getOffsetWidthForTabBar() - tabOffsetLeftAndWidth; scrollTargetDetermined = tabRightOffset > frameOffsetAndTabWidth; } @@ -194,7 +194,7 @@ export class MDCTabBarScrollerFoundation extends MDCFoundation this.shiftFrame_()); } @@ -213,7 +213,7 @@ export class MDCTabBarScrollerFoundation extends MDCFoundation void; @@ -34,7 +34,7 @@ export interface MDCTabBarAdapter { getOffsetWidth: () => number; setStyleForIndicator: (propertyName: string, value: string) => void; getOffsetWidthForIndicator: () => number; - notifyChange: (evtData: MDCTabBarEventDetail) => void; + notifyChange: (evtData: MDCTabBarChangeEventDetail) => void; getNumberOfTabs: () => number; isTabActiveAtIndex: (index: number) => boolean; setTabActiveAtIndex: (index: number, isActive: boolean) => void; @@ -44,5 +44,3 @@ export interface MDCTabBarAdapter { getComputedWidthForTabAtIndex: (index: number) => number; getComputedLeftForTabAtIndex: (index: number) => number; } - -export default MDCTabBarAdapter; diff --git a/packages/mdc-tabs/tab-bar/component.ts b/packages/mdc-tabs/tab-bar/component.ts index 8ff92804580..9982e337218 100644 --- a/packages/mdc-tabs/tab-bar/component.ts +++ b/packages/mdc-tabs/tab-bar/component.ts @@ -25,8 +25,11 @@ import {MDCComponent} from '@material/base/component'; import {MDCTab, MDCTabFactory, MDCTabFoundation, MDCTabSelectedEvent} from '../tab/index'; import {MDCTabBarAdapter} from './adapter'; import {MDCTabBarFoundation} from './foundation'; +import {MDCTabBarChangeEventDetail} from './types'; -export type MDCTabBarFactory = (el: Element) => MDCTabBar; +const {strings} = MDCTabBarFoundation; + +export type MDCTabBarFactory = (el: Element, foundation?: MDCTabBarFoundation) => MDCTabBar; export class MDCTabBar extends MDCComponent { static attachTo(root: Element) { @@ -61,7 +64,7 @@ export class MDCTabBar extends MDCComponent { private tabSelectedHandler_!: (evt: MDCTabSelectedEvent) => void; // assigned in initialize() initialize(tabFactory: MDCTabFactory = (el) => new MDCTab(el)) { - this.indicator_ = this.root_.querySelector(MDCTabBarFoundation.strings.INDICATOR_SELECTOR)!; + this.indicator_ = this.root_.querySelector(strings.INDICATOR_SELECTOR)!; this.tabs_ = this.gatherTabs_(tabFactory); this.tabSelectedHandler_ = ({detail}) => { const {tab} = detail; @@ -70,20 +73,22 @@ export class MDCTabBar extends MDCComponent { } getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. // tslint:disable:object-literal-sort-keys const adapter: MDCTabBarAdapter = { addClass: (className) => this.root_.classList.add(className), removeClass: (className) => this.root_.classList.remove(className), bindOnMDCTabSelectedEvent: () => - this.listen(MDCTabFoundation.strings.SELECTED_EVENT, this.tabSelectedHandler_), + this.listen(MDCTabFoundation.strings.SELECTED_EVENT, this.tabSelectedHandler_), unbindOnMDCTabSelectedEvent: () => - this.unlisten(MDCTabFoundation.strings.SELECTED_EVENT, this.tabSelectedHandler_), + this.unlisten(MDCTabFoundation.strings.SELECTED_EVENT, this.tabSelectedHandler_), registerResizeHandler: (handler) => window.addEventListener('resize', handler), deregisterResizeHandler: (handler) => window.removeEventListener('resize', handler), getOffsetWidth: () => this.root_.offsetWidth, setStyleForIndicator: (propertyName, value) => this.indicator_.style.setProperty(propertyName, value), getOffsetWidthForIndicator: () => this.indicator_.offsetWidth, - notifyChange: (evtData) => this.emit(MDCTabBarFoundation.strings.CHANGE_EVENT, evtData), + notifyChange: (evtData) => this.emit(strings.CHANGE_EVENT, evtData), getNumberOfTabs: () => this.tabs.length, isTabActiveAtIndex: (index) => this.tabs[index].isActive, setTabActiveAtIndex: (index, isActive) => { @@ -107,7 +112,7 @@ export class MDCTabBar extends MDCComponent { private gatherTabs_(tabFactory: MDCTabFactory): MDCTab[] { const tabElements: HTMLElement[] = - [].slice.call(this.root_.querySelectorAll(MDCTabBarFoundation.strings.TAB_SELECTOR)); + [].slice.call(this.root_.querySelectorAll(strings.TAB_SELECTOR)); return tabElements.map((el: Element) => tabFactory(el)); } diff --git a/packages/mdc-tabs/tab-bar/foundation.ts b/packages/mdc-tabs/tab-bar/foundation.ts index 4acae3fbe60..14266ea7269 100644 --- a/packages/mdc-tabs/tab-bar/foundation.ts +++ b/packages/mdc-tabs/tab-bar/foundation.ts @@ -21,7 +21,7 @@ * THE SOFTWARE. */ -import {getCorrectPropertyName} from '@material/animation/index'; +import {getCorrectPropertyName} from '@material/animation/util'; import {SpecificEventListener} from '@material/base'; import {MDCFoundation} from '@material/base/foundation'; import {MDCTabBarAdapter} from './adapter'; @@ -142,7 +142,7 @@ export class MDCTabBarFoundation extends MDCFoundation { const translateAmtForActiveTabLeft = this.adapter_.getComputedLeftForTabAtIndex(this.activeTabIndex_); const scaleAmtForActiveTabWidth = - this.adapter_.getComputedWidthForTabAtIndex(this.activeTabIndex_) / this.adapter_.getOffsetWidth(); + this.adapter_.getComputedWidthForTabAtIndex(this.activeTabIndex_) / this.adapter_.getOffsetWidth(); const transformValue = `translateX(${translateAmtForActiveTabLeft}px) scale(${scaleAmtForActiveTabWidth}, 1)`; this.adapter_.setStyleForIndicator(getCorrectPropertyName(window, 'transform'), transformValue); diff --git a/packages/mdc-tabs/tab-bar/types.ts b/packages/mdc-tabs/tab-bar/types.ts index 20e32139db4..542af1319d3 100644 --- a/packages/mdc-tabs/tab-bar/types.ts +++ b/packages/mdc-tabs/tab-bar/types.ts @@ -21,8 +21,12 @@ * THE SOFTWARE. */ -export type MDCTabBarEvent = CustomEvent; - -export interface MDCTabBarEventDetail { +export interface MDCTabBarChangeEventDetail { activeTabIndex: number; } + +// Note: CustomEvent is not supported by Closure Compiler. + +export interface MDCTabBarChangeEvent extends Event { + readonly detail: MDCTabBarChangeEventDetail; +} diff --git a/packages/mdc-tabs/tab/adapter.ts b/packages/mdc-tabs/tab/adapter.ts index a4b504d2c1d..f45a66ce4b0 100644 --- a/packages/mdc-tabs/tab/adapter.ts +++ b/packages/mdc-tabs/tab/adapter.ts @@ -32,5 +32,3 @@ export interface MDCTabAdapter { getOffsetLeft: () => number; notifySelected: () => void; } - -export default MDCTabAdapter; diff --git a/packages/mdc-tabs/tab/component.ts b/packages/mdc-tabs/tab/component.ts index 44799c33d83..650ae00fa55 100644 --- a/packages/mdc-tabs/tab/component.ts +++ b/packages/mdc-tabs/tab/component.ts @@ -22,18 +22,23 @@ */ import {MDCComponent} from '@material/base/component'; -import {MDCRipple} from '@material/ripple/index'; +import {MDCRipple} from '@material/ripple/component'; +import {MDCTabAdapter} from './adapter'; import {cssClasses} from './constants'; import {MDCTabFoundation} from './foundation'; -export type MDCTabFactory = (el: Element) => MDCTab; - -export type MDCTabSelectedEvent = CustomEvent; +export type MDCTabFactory = (el: Element, foundation?: MDCTabFoundation) => MDCTab; export interface MDCTabSelectedEventDetail { tab: MDCTab; } +// Note: CustomEvent is not supported by Closure Compiler. + +export interface MDCTabSelectedEvent extends Event { + readonly detail: MDCTabSelectedEventDetail; +} + export class MDCTab extends MDCComponent { static attachTo(root: Element) { return new MDCTab(root); @@ -73,18 +78,21 @@ export class MDCTab extends MDCComponent { } getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. // tslint:disable:object-literal-sort-keys - return new MDCTabFoundation({ + const adapter: MDCTabAdapter = { addClass: (className) => this.root_.classList.add(className), removeClass: (className) => this.root_.classList.remove(className), - registerInteractionHandler: (type, handler) => this.root_.addEventListener(type, handler), - deregisterInteractionHandler: (type, handler) => this.root_.removeEventListener(type, handler), + registerInteractionHandler: (type, handler) => this.listen(type, handler), + deregisterInteractionHandler: (type, handler) => this.unlisten(type, handler), getOffsetWidth: () => this.root_.offsetWidth, getOffsetLeft: () => this.root_.offsetLeft, notifySelected: () => - this.emit(MDCTabFoundation.strings.SELECTED_EVENT, {tab: this}, true), - }); + this.emit(MDCTabFoundation.strings.SELECTED_EVENT, {tab: this}, true), + }; // tslint:enable:object-literal-sort-keys + return new MDCTabFoundation(adapter); } initialSyncWithDOM() { diff --git a/packages/mdc-textfield/_mixins.scss b/packages/mdc-textfield/_mixins.scss index eac7ca16ee1..0134f968728 100644 --- a/packages/mdc-textfield/_mixins.scss +++ b/packages/mdc-textfield/_mixins.scss @@ -439,6 +439,7 @@ // Textarea @mixin mdc-text-field-textarea-disabled_ { @include mdc-text-field-outlined-disabled_; + /* @alternate */ @include mdc-text-field-textarea-fill-color_($mdc-textarea-disabled-background); } diff --git a/packages/mdc-textfield/adapter.ts b/packages/mdc-textfield/adapter.ts index c8163f1f1c3..c54351f0422 100644 --- a/packages/mdc-textfield/adapter.ts +++ b/packages/mdc-textfield/adapter.ts @@ -22,7 +22,7 @@ */ import {EventType, SpecificEventListener} from '@material/base/types'; -import {NativeInputElement} from './types'; +import {MDCTextFieldNativeInputElement} from './types'; /** * Defines the shape of the adapter expected by the foundation. @@ -31,7 +31,14 @@ import {NativeInputElement} from './types'; * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCTextFieldAdapter { +export interface MDCTextFieldAdapter extends MDCTextFieldRootAdapter, + MDCTextFieldInputAdapter, + MDCTextFieldLabelAdapter, + MDCTextFieldLineRippleAdapter, + MDCTextFieldOutlineAdapter { +} + +export interface MDCTextFieldRootAdapter { /** * Adds a class to the root Element. */ @@ -50,22 +57,12 @@ interface MDCTextFieldAdapter { /** * Registers an event handler on the root element for a given event. */ - registerTextFieldInteractionHandler(evtType: E, handler: SpecificEventListener): void; + registerTextFieldInteractionHandler(evtType: K, handler: SpecificEventListener): void; /** * Deregisters an event handler on the root element for a given event. */ - deregisterTextFieldInteractionHandler(evtType: E, handler: SpecificEventListener): void; - - /** - * Registers an event listener on the native input element for a given event. - */ - registerInputInteractionHandler(evtType: E, handler: SpecificEventListener): void; - - /** - * Deregisters an event listener on the native input element for a given event. - */ - deregisterInputInteractionHandler(evtType: E, handler: SpecificEventListener): void; + deregisterTextFieldInteractionHandler(evtType: K, handler: SpecificEventListener): void; /** * Registers a validation attribute change listener on the input element. @@ -77,12 +74,14 @@ interface MDCTextFieldAdapter { * Disconnects a validation attribute observer on the input element. */ deregisterValidationAttributeChangeHandler(observer: MutationObserver): void; +} +export interface MDCTextFieldInputAdapter { /** * @return The native `` element, or an object with the same shape. * Note that this method can return null, which the foundation will handle gracefully. */ - getNativeInput(): NativeInputElement | null; + getNativeInput(): MDCTextFieldNativeInputElement | null; /** * @return true if the textfield is focused. We achieve this via `document.activeElement === this.root_`. @@ -90,20 +89,17 @@ interface MDCTextFieldAdapter { isFocused(): boolean; /** - * Activates the line ripple. - */ - activateLineRipple(): void; - - /** - * Deactivates the line ripple. + * Registers an event listener on the native input element for a given event. */ - deactivateLineRipple(): void; + registerInputInteractionHandler(evtType: K, handler: SpecificEventListener): void; /** - * Sets the transform origin of the line ripple. + * Deregisters an event listener on the native input element for a given event. */ - setLineRippleTransformOrigin(normalizedX: number): void; + deregisterInputInteractionHandler(evtType: K, handler: SpecificEventListener): void; +} +export interface MDCTextFieldLabelAdapter { /** * Only implement if label exists. * Shakes label if shouldShake is true. @@ -126,7 +122,26 @@ interface MDCTextFieldAdapter { * @return width of label in pixels. */ getLabelWidth(): number; +} + +export interface MDCTextFieldLineRippleAdapter { + /** + * Activates the line ripple. + */ + activateLineRipple(): void; + + /** + * Deactivates the line ripple. + */ + deactivateLineRipple(): void; + + /** + * Sets the transform origin of the line ripple. + */ + setLineRippleTransformOrigin(normalizedX: number): void; +} +export interface MDCTextFieldOutlineAdapter { /** * @return true if outline element exists, false if it doesn't. */ @@ -143,5 +158,3 @@ interface MDCTextFieldAdapter { */ closeOutline(): void; } - -export {MDCTextFieldAdapter as default, MDCTextFieldAdapter}; diff --git a/packages/mdc-textfield/character-counter/adapter.ts b/packages/mdc-textfield/character-counter/adapter.ts index 74351c64cd8..f1d2d26b98b 100644 --- a/packages/mdc-textfield/character-counter/adapter.ts +++ b/packages/mdc-textfield/character-counter/adapter.ts @@ -28,11 +28,9 @@ * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCTextFieldCharacterCounterAdapter { +export interface MDCTextFieldCharacterCounterAdapter { /** * Sets the text content of character counter element. */ setContent(content: string): void; } - -export {MDCTextFieldCharacterCounterAdapter as default, MDCTextFieldCharacterCounterAdapter}; diff --git a/packages/mdc-textfield/character-counter/component.ts b/packages/mdc-textfield/character-counter/component.ts new file mode 100644 index 00000000000..b0395040a2c --- /dev/null +++ b/packages/mdc-textfield/character-counter/component.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright 2019 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {MDCTextFieldCharacterCounterAdapter} from './adapter'; +import {MDCTextFieldCharacterCounterFoundation} from './foundation'; + +export type MDCTextFieldCharacterCounterFactory = + (el: Element, foundation?: MDCTextFieldCharacterCounterFoundation) => MDCTextFieldCharacterCounter; + +export class MDCTextFieldCharacterCounter extends MDCComponent { + static attachTo(root: Element): MDCTextFieldCharacterCounter { + return new MDCTextFieldCharacterCounter(root); + } + + get foundation(): MDCTextFieldCharacterCounterFoundation { + return this.foundation_; + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + const adapter: MDCTextFieldCharacterCounterAdapter = { + setContent: (content) => { + this.root_.textContent = content; + }, + }; + return new MDCTextFieldCharacterCounterFoundation(adapter); + } +} diff --git a/packages/mdc-textfield/character-counter/foundation.ts b/packages/mdc-textfield/character-counter/foundation.ts index a9c7f8058ae..2071d032544 100644 --- a/packages/mdc-textfield/character-counter/foundation.ts +++ b/packages/mdc-textfield/character-counter/foundation.ts @@ -25,7 +25,7 @@ import {MDCFoundation} from '@material/base/foundation'; import {MDCTextFieldCharacterCounterAdapter} from './adapter'; import {cssClasses, strings} from './constants'; -class MDCTextFieldCharacterCounterFoundation extends MDCFoundation { +export class MDCTextFieldCharacterCounterFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } @@ -53,4 +53,4 @@ class MDCTextFieldCharacterCounterFoundation extends MDCFoundation { - static attachTo(root: Element): MDCTextFieldCharacterCounter { - return new MDCTextFieldCharacterCounter(root); - } - - get foundation(): MDCTextFieldCharacterCounterFoundation { - return this.foundation_; - } - - getDefaultFoundation(): MDCTextFieldCharacterCounterFoundation { - return new MDCTextFieldCharacterCounterFoundation({ - setContent: (content) => { this.root_.textContent = content; }, - }); - } -} - -export {MDCTextFieldCharacterCounter as default, MDCTextFieldCharacterCounter}; export * from './adapter'; +export * from './component'; export * from './foundation'; diff --git a/packages/mdc-textfield/component.ts b/packages/mdc-textfield/component.ts new file mode 100644 index 00000000000..37137a96898 --- /dev/null +++ b/packages/mdc-textfield/component.ts @@ -0,0 +1,461 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import * as ponyfill from '@material/dom/ponyfill'; +import {MDCFloatingLabel, MDCFloatingLabelFactory} from '@material/floating-label/index'; +import {MDCLineRipple, MDCLineRippleFactory} from '@material/line-ripple/index'; +import {MDCNotchedOutline, MDCNotchedOutlineFactory} from '@material/notched-outline/index'; +import {MDCRippleAdapter} from '@material/ripple/adapter'; +import {MDCRipple, MDCRippleFactory} from '@material/ripple/component'; +import {MDCRippleFoundation} from '@material/ripple/foundation'; +import {MDCRippleCapableSurface} from '@material/ripple/types'; +import { + MDCTextFieldAdapter, + MDCTextFieldInputAdapter, + MDCTextFieldLabelAdapter, + MDCTextFieldLineRippleAdapter, + MDCTextFieldOutlineAdapter, + MDCTextFieldRootAdapter, +} from './adapter'; +import { + MDCTextFieldCharacterCounter, + MDCTextFieldCharacterCounterFactory, + MDCTextFieldCharacterCounterFoundation, +} from './character-counter/index'; +import {cssClasses, strings} from './constants'; +import {MDCTextFieldFoundation} from './foundation'; +import { + MDCTextFieldHelperText, + MDCTextFieldHelperTextFactory, + MDCTextFieldHelperTextFoundation, +} from './helper-text/index'; +import {MDCTextFieldIcon, MDCTextFieldIconFactory} from './icon/index'; +import {MDCTextFieldFoundationMap} from './types'; + +export class MDCTextField extends MDCComponent implements MDCRippleCapableSurface { + static attachTo(root: Element): MDCTextField { + return new MDCTextField(root); + } + + // Public visibility for these properties is required by MDCRippleCapableSurface. + root_!: HTMLElement; // assigned in MDCComponent constructor + ripple!: MDCRipple | null; // assigned in initialize() + + // The only required sub-element. + private input_!: HTMLInputElement; // assigned in initialize() + + // Optional sub-elements. + private characterCounter_!: MDCTextFieldCharacterCounter | null; // assigned in initialize() + private helperText_!: MDCTextFieldHelperText | null; // assigned in initialize() + private label_!: MDCFloatingLabel | null; // assigned in initialize() + private leadingIcon_!: MDCTextFieldIcon | null; // assigned in initialize() + private lineRipple_!: MDCLineRipple | null; // assigned in initialize() + private outline_!: MDCNotchedOutline | null; // assigned in initialize() + private trailingIcon_!: MDCTextFieldIcon | null; // assigned in initialize() + + initialize( + rippleFactory: MDCRippleFactory = (el, foundation) => new MDCRipple(el, foundation), + lineRippleFactory: MDCLineRippleFactory = (el) => new MDCLineRipple(el), + helperTextFactory: MDCTextFieldHelperTextFactory = (el) => new MDCTextFieldHelperText(el), + characterCounterFactory: MDCTextFieldCharacterCounterFactory = (el) => new MDCTextFieldCharacterCounter(el), + iconFactory: MDCTextFieldIconFactory = (el) => new MDCTextFieldIcon(el), + labelFactory: MDCFloatingLabelFactory = (el) => new MDCFloatingLabel(el), + outlineFactory: MDCNotchedOutlineFactory = (el) => new MDCNotchedOutline(el), + ) { + this.input_ = this.root_.querySelector(strings.INPUT_SELECTOR)!; + + const labelElement = this.root_.querySelector(strings.LABEL_SELECTOR); + this.label_ = labelElement ? labelFactory(labelElement) : null; + + const lineRippleElement = this.root_.querySelector(strings.LINE_RIPPLE_SELECTOR); + this.lineRipple_ = lineRippleElement ? lineRippleFactory(lineRippleElement) : null; + + const outlineElement = this.root_.querySelector(strings.OUTLINE_SELECTOR); + this.outline_ = outlineElement ? outlineFactory(outlineElement) : null; + + // Helper text + const helperTextStrings = MDCTextFieldHelperTextFoundation.strings; + const nextElementSibling = this.root_.nextElementSibling; + const hasHelperLine = (nextElementSibling && nextElementSibling.classList.contains(cssClasses.HELPER_LINE)); + const helperTextEl = + hasHelperLine && nextElementSibling && nextElementSibling.querySelector(helperTextStrings.ROOT_SELECTOR); + this.helperText_ = helperTextEl ? helperTextFactory(helperTextEl) : null; + + // Character counter + const characterCounterStrings = MDCTextFieldCharacterCounterFoundation.strings; + let characterCounterEl = this.root_.querySelector(characterCounterStrings.ROOT_SELECTOR); + // If character counter is not found in root element search in sibling element. + if (!characterCounterEl && hasHelperLine && nextElementSibling) { + characterCounterEl = nextElementSibling.querySelector(characterCounterStrings.ROOT_SELECTOR); + } + this.characterCounter_ = characterCounterEl ? characterCounterFactory(characterCounterEl) : null; + + this.leadingIcon_ = null; + this.trailingIcon_ = null; + const iconElements = this.root_.querySelectorAll(strings.ICON_SELECTOR); + if (iconElements.length > 0) { + if (iconElements.length > 1) { // Has both icons. + this.leadingIcon_ = iconFactory(iconElements[0]); + this.trailingIcon_ = iconFactory(iconElements[1]); + } else { + if (this.root_.classList.contains(cssClasses.WITH_LEADING_ICON)) { + this.leadingIcon_ = iconFactory(iconElements[0]); + } else { + this.trailingIcon_ = iconFactory(iconElements[0]); + } + } + } + + this.ripple = this.createRipple_(rippleFactory); + } + + destroy() { + if (this.ripple) { + this.ripple.destroy(); + } + if (this.lineRipple_) { + this.lineRipple_.destroy(); + } + if (this.helperText_) { + this.helperText_.destroy(); + } + if (this.characterCounter_) { + this.characterCounter_.destroy(); + } + if (this.leadingIcon_) { + this.leadingIcon_.destroy(); + } + if (this.trailingIcon_) { + this.trailingIcon_.destroy(); + } + if (this.label_) { + this.label_.destroy(); + } + if (this.outline_) { + this.outline_.destroy(); + } + super.destroy(); + } + + /** + * Initializes the Text Field's internal state based on the environment's + * state. + */ + initialSyncWithDom() { + this.disabled = this.input_.disabled; + } + + get value(): string { + return this.foundation_.getValue(); + } + + /** + * @param value The value to set on the input. + */ + set value(value: string) { + this.foundation_.setValue(value); + } + + get disabled(): boolean { + return this.foundation_.isDisabled(); + } + + /** + * @param disabled Sets the Text Field disabled or enabled. + */ + set disabled(disabled: boolean) { + this.foundation_.setDisabled(disabled); + } + + get valid(): boolean { + return this.foundation_.isValid(); + } + + /** + * @param valid Sets the Text Field valid or invalid. + */ + set valid(valid: boolean) { + this.foundation_.setValid(valid); + } + + get required(): boolean { + return this.input_.required; + } + + /** + * @param required Sets the Text Field to required. + */ + set required(required: boolean) { + this.input_.required = required; + } + + get pattern(): string { + return this.input_.pattern; + } + + /** + * @param pattern Sets the input element's validation pattern. + */ + set pattern(pattern: string) { + this.input_.pattern = pattern; + } + + get minLength(): number { + return this.input_.minLength; + } + + /** + * @param minLength Sets the input element's minLength. + */ + set minLength(minLength: number) { + this.input_.minLength = minLength; + } + + get maxLength(): number { + return this.input_.maxLength; + } + + /** + * @param maxLength Sets the input element's maxLength. + */ + set maxLength(maxLength: number) { + // Chrome throws exception if maxLength is set to a value less than zero + if (maxLength < 0) { + this.input_.removeAttribute('maxLength'); + } else { + this.input_.maxLength = maxLength; + } + } + + get min(): string { + return this.input_.min; + } + + /** + * @param min Sets the input element's min. + */ + set min(min: string) { + this.input_.min = min; + } + + get max(): string { + return this.input_.max; + } + + /** + * @param max Sets the input element's max. + */ + set max(max: string) { + this.input_.max = max; + } + + get step(): string { + return this.input_.step; + } + + /** + * @param step Sets the input element's step. + */ + set step(step: string) { + this.input_.step = step; + } + + /** + * Sets the helper text element content. + */ + set helperTextContent(content: string) { + this.foundation_.setHelperTextContent(content); + } + + /** + * Sets the aria label of the leading icon. + */ + set leadingIconAriaLabel(label: string) { + this.foundation_.setLeadingIconAriaLabel(label); + } + + /** + * Sets the text content of the leading icon. + */ + set leadingIconContent(content: string) { + this.foundation_.setLeadingIconContent(content); + } + + /** + * Sets the aria label of the trailing icon. + */ + set trailingIconAriaLabel(label: string) { + this.foundation_.setTrailingIconAriaLabel(label); + } + + /** + * Sets the text content of the trailing icon. + */ + set trailingIconContent(content: string) { + this.foundation_.setTrailingIconContent(content); + } + + /** + * Enables or disables the use of native validation. Use this for custom validation. + * @param useNativeValidation Set this to false to ignore native input validation. + */ + set useNativeValidation(useNativeValidation: boolean) { + this.foundation_.setUseNativeValidation(useNativeValidation); + } + + /** + * Focuses the input element. + */ + focus() { + this.input_.focus(); + } + + /** + * Recomputes the outline SVG path for the outline element. + */ + layout() { + const openNotch = this.foundation_.shouldFloat; + this.foundation_.notchOutline(openNotch); + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + // tslint:disable:object-literal-sort-keys + const adapter: MDCTextFieldAdapter = { + ...this.getRootAdapterMethods_(), + ...this.getInputAdapterMethods_(), + ...this.getLabelAdapterMethods_(), + ...this.getLineRippleAdapterMethods_(), + ...this.getOutlineAdapterMethods_(), + }; + // tslint:enable:object-literal-sort-keys + return new MDCTextFieldFoundation(adapter, this.getFoundationMap_()); + } + + private getRootAdapterMethods_(): MDCTextFieldRootAdapter { + // tslint:disable:object-literal-sort-keys + return { + addClass: (className) => this.root_.classList.add(className), + removeClass: (className) => this.root_.classList.remove(className), + hasClass: (className) => this.root_.classList.contains(className), + registerTextFieldInteractionHandler: (evtType, handler) => this.listen(evtType, handler), + deregisterTextFieldInteractionHandler: (evtType, handler) => this.unlisten(evtType, handler), + registerValidationAttributeChangeHandler: (handler) => { + const getAttributesList = (mutationsList: MutationRecord[]): string[] => { + return mutationsList + .map((mutation) => mutation.attributeName) + .filter((attributeName) => attributeName) as string[]; + }; + const observer = new MutationObserver((mutationsList) => handler(getAttributesList(mutationsList))); + const config = {attributes: true}; + observer.observe(this.input_, config); + return observer; + }, + deregisterValidationAttributeChangeHandler: (observer) => observer.disconnect(), + }; + // tslint:enable:object-literal-sort-keys + } + + private getInputAdapterMethods_(): MDCTextFieldInputAdapter { + // tslint:disable:object-literal-sort-keys + return { + getNativeInput: () => this.input_, + isFocused: () => document.activeElement === this.input_, + registerInputInteractionHandler: (evtType, handler) => this.input_.addEventListener(evtType, handler), + deregisterInputInteractionHandler: (evtType, handler) => this.input_.removeEventListener(evtType, handler), + }; + // tslint:enable:object-literal-sort-keys + } + + private getLabelAdapterMethods_(): MDCTextFieldLabelAdapter { + return { + floatLabel: (shouldFloat) => this.label_ && this.label_.float(shouldFloat), + getLabelWidth: () => this.label_ ? this.label_.getWidth() : 0, + hasLabel: () => Boolean(this.label_), + shakeLabel: (shouldShake) => this.label_ && this.label_.shake(shouldShake), + }; + } + + private getLineRippleAdapterMethods_(): MDCTextFieldLineRippleAdapter { + return { + activateLineRipple: () => { + if (this.lineRipple_) { + this.lineRipple_.activate(); + } + }, + deactivateLineRipple: () => { + if (this.lineRipple_) { + this.lineRipple_.deactivate(); + } + }, + setLineRippleTransformOrigin: (normalizedX) => { + if (this.lineRipple_) { + this.lineRipple_.setRippleCenter(normalizedX); + } + }, + }; + } + + private getOutlineAdapterMethods_(): MDCTextFieldOutlineAdapter { + return { + closeOutline: () => this.outline_ && this.outline_.closeNotch(), + hasOutline: () => Boolean(this.outline_), + notchOutline: (labelWidth) => this.outline_ && this.outline_.notch(labelWidth), + }; + } + + /** + * @return A map of all subcomponents to subfoundations. + */ + private getFoundationMap_(): Partial { + return { + characterCounter: this.characterCounter_ ? this.characterCounter_.foundation : undefined, + helperText: this.helperText_ ? this.helperText_.foundation : undefined, + leadingIcon: this.leadingIcon_ ? this.leadingIcon_.foundation : undefined, + trailingIcon: this.trailingIcon_ ? this.trailingIcon_.foundation : undefined, + }; + } + + private createRipple_(rippleFactory: MDCRippleFactory): MDCRipple | null { + const isTextArea = this.root_.classList.contains(cssClasses.TEXTAREA); + const isOutlined = this.root_.classList.contains(cssClasses.OUTLINED); + + if (isTextArea || isOutlined) { + return null; + } + + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + // tslint:disable:object-literal-sort-keys + const adapter: MDCRippleAdapter = { + ...MDCRipple.createAdapter(this), + isSurfaceActive: () => ponyfill.matches(this.input_, ':active'), + registerInteractionHandler: (evtType, handler) => this.input_.addEventListener(evtType, handler), + deregisterInteractionHandler: (evtType, handler) => this.input_.removeEventListener(evtType, handler), + }; + // tslint:enable:object-literal-sort-keys + return rippleFactory(this.root_, new MDCRippleFoundation(adapter)); + } +} diff --git a/packages/mdc-textfield/foundation.ts b/packages/mdc-textfield/foundation.ts index acdbf0c840d..b1fd1a3a098 100644 --- a/packages/mdc-textfield/foundation.ts +++ b/packages/mdc-textfield/foundation.ts @@ -24,11 +24,11 @@ import {MDCFoundation} from '@material/base/foundation'; import {SpecificEventListener} from '@material/base/types'; import {MDCTextFieldAdapter} from './adapter'; -import {MDCTextFieldCharacterCounterFoundation} from './character-counter'; +import {MDCTextFieldCharacterCounterFoundation} from './character-counter/foundation'; import {ALWAYS_FLOAT_TYPES, cssClasses, numbers, strings, VALIDATION_ATTR_WHITELIST} from './constants'; -import {MDCTextFieldHelperTextFoundation} from './helper-text'; -import {MDCTextFieldIconFoundation} from './icon'; -import {FoundationMapType, NativeInputElement} from './types'; +import {MDCTextFieldHelperTextFoundation} from './helper-text/foundation'; +import {MDCTextFieldIconFoundation} from './icon/foundation'; +import {MDCTextFieldFoundationMap, MDCTextFieldNativeInputElement} from './types'; type PointerDownEventType = 'mousedown' | 'touchstart'; type InteractionEventType = 'click' | 'keydown'; @@ -36,7 +36,7 @@ type InteractionEventType = 'click' | 'keydown'; const POINTERDOWN_EVENTS: PointerDownEventType[] = ['mousedown', 'touchstart']; const INTERACTION_EVENTS: InteractionEventType[] = ['click', 'keydown']; -class MDCTextFieldFoundation extends MDCFoundation { +export class MDCTextFieldFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } @@ -115,7 +115,7 @@ class MDCTextFieldFoundation extends MDCFoundation { * @param adapter * @param foundationMap Map from subcomponent names to their subfoundations. */ - constructor(adapter?: Partial, foundationMap: Partial = {}) { + constructor(adapter?: Partial, foundationMap: Partial = {}) { super({...MDCTextFieldFoundation.defaultAdapter, ...adapter}); this.helperText_ = foundationMap.helperText; @@ -304,7 +304,7 @@ class MDCTextFieldFoundation extends MDCFoundation { */ isValid(): boolean { return this.useNativeValidation_ - ? this.isNativeInputValid_() : this.isValid_; + ? this.isNativeInputValid_() : this.isValid_; } /** @@ -465,7 +465,7 @@ class MDCTextFieldFoundation extends MDCFoundation { /** * @return The native text input element from the host environment, or an object with the same shape for unit tests. */ - private getNativeInput_(): NativeInputElement { + private getNativeInput_(): MDCTextFieldNativeInputElement { // this.adapter_ may be undefined in foundation unit tests. This happens when testdouble is creating a mock object // and invokes the shouldShake/shouldFloat getters (which in turn call getValue(), which calls this method) before // init() has been called from the MDCTextField constructor. To work around that issue, we return a dummy object. @@ -483,4 +483,4 @@ class MDCTextFieldFoundation extends MDCFoundation { } } -export {MDCTextFieldFoundation as default, MDCTextFieldFoundation}; +export default MDCTextFieldFoundation; diff --git a/packages/mdc-textfield/helper-text/adapter.ts b/packages/mdc-textfield/helper-text/adapter.ts index bdc90c6c5d5..d9c777dc044 100644 --- a/packages/mdc-textfield/helper-text/adapter.ts +++ b/packages/mdc-textfield/helper-text/adapter.ts @@ -28,7 +28,7 @@ * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCTextFieldHelperTextAdapter { +export interface MDCTextFieldHelperTextAdapter { /** * Adds a class to the helper text element. */ @@ -59,5 +59,3 @@ interface MDCTextFieldHelperTextAdapter { */ setContent(content: string): void; } - -export {MDCTextFieldHelperTextAdapter as default, MDCTextFieldHelperTextAdapter}; diff --git a/packages/mdc-textfield/helper-text/component.ts b/packages/mdc-textfield/helper-text/component.ts new file mode 100644 index 00000000000..f2237bf056e --- /dev/null +++ b/packages/mdc-textfield/helper-text/component.ts @@ -0,0 +1,57 @@ +/** + * @license + * Copyright 2017 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {MDCTextFieldHelperTextAdapter} from './adapter'; +import {MDCTextFieldHelperTextFoundation} from './foundation'; + +export type MDCTextFieldHelperTextFactory = + (el: Element, foundation?: MDCTextFieldHelperTextFoundation) => MDCTextFieldHelperText; + +export class MDCTextFieldHelperText extends MDCComponent { + static attachTo(root: Element): MDCTextFieldHelperText { + return new MDCTextFieldHelperText(root); + } + + get foundation(): MDCTextFieldHelperTextFoundation { + return this.foundation_; + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + // tslint:disable:object-literal-sort-keys + const adapter: MDCTextFieldHelperTextAdapter = { + addClass: (className) => this.root_.classList.add(className), + removeClass: (className) => this.root_.classList.remove(className), + hasClass: (className) => this.root_.classList.contains(className), + setAttr: (attr, value) => this.root_.setAttribute(attr, value), + removeAttr: (attr) => this.root_.removeAttribute(attr), + setContent: (content) => { + this.root_.textContent = content; + }, + }; + // tslint:enable:object-literal-sort-keys + return new MDCTextFieldHelperTextFoundation(adapter); + } +} diff --git a/packages/mdc-textfield/helper-text/foundation.ts b/packages/mdc-textfield/helper-text/foundation.ts index 291b173bbd5..f65517e2983 100644 --- a/packages/mdc-textfield/helper-text/foundation.ts +++ b/packages/mdc-textfield/helper-text/foundation.ts @@ -25,7 +25,7 @@ import {MDCFoundation} from '@material/base/foundation'; import {MDCTextFieldHelperTextAdapter} from './adapter'; import {cssClasses, strings} from './constants'; -class MDCTextFieldHelperTextFoundation extends MDCFoundation { +export class MDCTextFieldHelperTextFoundation extends MDCFoundation { static get cssClasses() { return cssClasses; } @@ -117,4 +117,4 @@ class MDCTextFieldHelperTextFoundation extends MDCFoundation { - static attachTo(root: Element): MDCTextFieldHelperText { - return new MDCTextFieldHelperText(root); - } - - get foundation(): MDCTextFieldHelperTextFoundation { - return this.foundation_; - } - - getDefaultFoundation(): MDCTextFieldHelperTextFoundation { - // tslint:disable:object-literal-sort-keys - return new MDCTextFieldHelperTextFoundation({ - addClass: (className) => this.root_.classList.add(className), - removeClass: (className) => this.root_.classList.remove(className), - hasClass: (className) => this.root_.classList.contains(className), - setAttr: (attr, value) => this.root_.setAttribute(attr, value), - removeAttr: (attr) => this.root_.removeAttribute(attr), - setContent: (content) => { this.root_.textContent = content; }, - }); - // tslint:enable:object-literal-sort-keys - } -} - -export {MDCTextFieldHelperText as default, MDCTextFieldHelperText}; export * from './adapter'; +export * from './component'; export * from './foundation'; diff --git a/packages/mdc-textfield/icon/adapter.ts b/packages/mdc-textfield/icon/adapter.ts index 0516ed5a70f..71a6bb4bf19 100644 --- a/packages/mdc-textfield/icon/adapter.ts +++ b/packages/mdc-textfield/icon/adapter.ts @@ -30,7 +30,7 @@ import {EventType, SpecificEventListener} from '@material/base/types'; * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCTextFieldIconAdapter { +export interface MDCTextFieldIconAdapter { /** * Gets the value of an attribute on the icon element. */ @@ -54,17 +54,15 @@ interface MDCTextFieldIconAdapter { /** * Registers an event listener on the icon element for a given event. */ - registerInteractionHandler(evtType: E, handler: SpecificEventListener): void; + registerInteractionHandler(evtType: K, handler: SpecificEventListener): void; /** * Deregisters an event listener on the icon element for a given event. */ - deregisterInteractionHandler(evtType: E, handler: SpecificEventListener): void; + deregisterInteractionHandler(evtType: K, handler: SpecificEventListener): void; /** * Emits a custom event "MDCTextField:icon" denoting a user has clicked the icon. */ notifyIconAction(): void; } - -export {MDCTextFieldIconAdapter as default, MDCTextFieldIconAdapter}; diff --git a/packages/mdc-textfield/icon/component.ts b/packages/mdc-textfield/icon/component.ts new file mode 100644 index 00000000000..ddaf9e45e71 --- /dev/null +++ b/packages/mdc-textfield/icon/component.ts @@ -0,0 +1,58 @@ +/** + * @license + * Copyright 2017 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {MDCTextFieldIconAdapter} from './adapter'; +import {MDCTextFieldIconFoundation} from './foundation'; + +export type MDCTextFieldIconFactory = (el: Element, foundation?: MDCTextFieldIconFoundation) => MDCTextFieldIcon; + +export class MDCTextFieldIcon extends MDCComponent { + static attachTo(root: Element): MDCTextFieldIcon { + return new MDCTextFieldIcon(root); + } + + get foundation(): MDCTextFieldIconFoundation { + return this.foundation_; + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + // tslint:disable:object-literal-sort-keys + const adapter: MDCTextFieldIconAdapter = { + getAttr: (attr) => this.root_.getAttribute(attr), + setAttr: (attr, value) => this.root_.setAttribute(attr, value), + removeAttr: (attr) => this.root_.removeAttribute(attr), + setContent: (content) => { + this.root_.textContent = content; + }, + registerInteractionHandler: (evtType, handler) => this.listen(evtType, handler), + deregisterInteractionHandler: (evtType, handler) => this.unlisten(evtType, handler), + notifyIconAction: () => this.emit( + MDCTextFieldIconFoundation.strings.ICON_EVENT, {} /* evtData */, true /* shouldBubble */), + }; + // tslint:enable:object-literal-sort-keys + return new MDCTextFieldIconFoundation(adapter); + } +} diff --git a/packages/mdc-textfield/icon/foundation.ts b/packages/mdc-textfield/icon/foundation.ts index 7b0dd71edc0..d8d54d69e77 100644 --- a/packages/mdc-textfield/icon/foundation.ts +++ b/packages/mdc-textfield/icon/foundation.ts @@ -30,7 +30,7 @@ type InteractionEventType = 'click' | 'keydown'; const INTERACTION_EVENTS: InteractionEventType[] = ['click', 'keydown']; -class MDCTextFieldIconFoundation extends MDCFoundation { +export class MDCTextFieldIconFoundation extends MDCFoundation { static get strings() { return strings; } @@ -105,4 +105,4 @@ class MDCTextFieldIconFoundation extends MDCFoundation } } -export {MDCTextFieldIconFoundation as default, MDCTextFieldIconFoundation}; +export default MDCTextFieldIconFoundation; diff --git a/packages/mdc-textfield/icon/index.ts b/packages/mdc-textfield/icon/index.ts index 2d19a8d5e1f..f8c89ac94f3 100644 --- a/packages/mdc-textfield/icon/index.ts +++ b/packages/mdc-textfield/icon/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,39 +21,6 @@ * THE SOFTWARE. */ -import {MDCComponent} from '@material/base/component'; -import {EventType, SpecificEventListener} from '@material/base/types'; -import {MDCTextFieldIconFoundation} from './foundation'; - -class MDCTextFieldIcon extends MDCComponent { - static attachTo(root: Element): MDCTextFieldIcon { - return new MDCTextFieldIcon(root); - } - - get foundation(): MDCTextFieldIconFoundation { - return this.foundation_; - } - - getDefaultFoundation(): MDCTextFieldIconFoundation { - // tslint:disable:object-literal-sort-keys - return new MDCTextFieldIconFoundation({ - getAttr: (attr) => this.root_.getAttribute(attr), - setAttr: (attr, value) => this.root_.setAttribute(attr, value), - removeAttr: (attr) => this.root_.removeAttribute(attr), - setContent: (content) => { this.root_.textContent = content; }, - registerInteractionHandler: (evtType: E, handler: SpecificEventListener) => { - this.root_.addEventListener(evtType, handler); - }, - deregisterInteractionHandler: (evtType: E, handler: SpecificEventListener) => { - this.root_.removeEventListener(evtType, handler); - }, - notifyIconAction: () => this.emit( - MDCTextFieldIconFoundation.strings.ICON_EVENT, {} /* evtData */, true /* shouldBubble */), - }); - // tslint:enable:object-literal-sort-keys - } -} - -export {MDCTextFieldIcon as default, MDCTextFieldIcon}; export * from './adapter'; +export * from './component'; export * from './foundation'; diff --git a/packages/mdc-textfield/index.ts b/packages/mdc-textfield/index.ts index 78870c12eb1..5e6399251c3 100644 --- a/packages/mdc-textfield/index.ts +++ b/packages/mdc-textfield/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2016 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,425 +21,8 @@ * THE SOFTWARE. */ -import {MDCComponent} from '@material/base/component'; -import {EventType, SpecificEventListener} from '@material/base/types'; -import * as ponyfill from '@material/dom/ponyfill'; -import {MDCFloatingLabel} from '@material/floating-label/index'; -import {MDCLineRipple} from '@material/line-ripple/index'; -import {MDCNotchedOutline} from '@material/notched-outline/index'; -import {MDCRippleFoundation} from '@material/ripple/foundation'; -import {MDCRipple} from '@material/ripple/index'; -import {RippleCapableSurface} from '@material/ripple/types'; -import {MDCTextFieldCharacterCounter, MDCTextFieldCharacterCounterFoundation} from './character-counter'; -import {cssClasses, strings} from './constants'; -import {MDCTextFieldFoundation} from './foundation'; -import {MDCTextFieldHelperText, MDCTextFieldHelperTextFoundation} from './helper-text'; -import {MDCTextFieldIcon} from './icon'; -import { - CharacterCounterFactory, - FoundationMapType, - HelperTextFactory, - IconFactory, - LabelFactory, - LineRippleFactory, - OutlineFactory, - RippleFactory, -} from './types'; - -class MDCTextField extends MDCComponent implements RippleCapableSurface { - static attachTo(root: Element): MDCTextField { - return new MDCTextField(root); - } - - // Public visibility for these properties is required by RippleCapableSurface. - root_!: HTMLElement; // assigned in MDCComponent constructor - ripple!: MDCRipple | null; // assigned in initialize() - - // The only required sub-element. - private input_!: HTMLInputElement; // assigned in initialize() - - // Optional sub-elements. - private characterCounter_!: MDCTextFieldCharacterCounter | null; // assigned in initialize() - private helperText_!: MDCTextFieldHelperText | null; // assigned in initialize() - private label_!: MDCFloatingLabel | null; // assigned in initialize() - private leadingIcon_!: MDCTextFieldIcon | null; // assigned in initialize() - private lineRipple_!: MDCLineRipple | null; // assigned in initialize() - private outline_!: MDCNotchedOutline | null; // assigned in initialize() - private trailingIcon_!: MDCTextFieldIcon | null; // assigned in initialize() - - initialize( - rippleFactory: RippleFactory = (el, foundation) => new MDCRipple(el, foundation), - lineRippleFactory: LineRippleFactory = (el) => new MDCLineRipple(el), - helperTextFactory: HelperTextFactory = (el) => new MDCTextFieldHelperText(el), - characterCounterFactory: CharacterCounterFactory = (el) => new MDCTextFieldCharacterCounter(el), - iconFactory: IconFactory = (el) => new MDCTextFieldIcon(el), - labelFactory: LabelFactory = (el) => new MDCFloatingLabel(el), - outlineFactory: OutlineFactory = (el) => new MDCNotchedOutline(el), - ) { - this.input_ = this.root_.querySelector(strings.INPUT_SELECTOR)!; - - const labelElement = this.root_.querySelector(strings.LABEL_SELECTOR); - this.label_ = labelElement ? labelFactory(labelElement) : null; - - const lineRippleElement = this.root_.querySelector(strings.LINE_RIPPLE_SELECTOR); - this.lineRipple_ = lineRippleElement ? lineRippleFactory(lineRippleElement) : null; - - const outlineElement = this.root_.querySelector(strings.OUTLINE_SELECTOR); - this.outline_ = outlineElement ? outlineFactory(outlineElement) : null; - - // Helper text - const helperTextStrings = MDCTextFieldHelperTextFoundation.strings; - const nextElementSibling = this.root_.nextElementSibling; - const hasHelperLine = (nextElementSibling && nextElementSibling.classList.contains(cssClasses.HELPER_LINE)); - const helperTextEl = - hasHelperLine && nextElementSibling && nextElementSibling.querySelector(helperTextStrings.ROOT_SELECTOR); - this.helperText_ = helperTextEl ? helperTextFactory(helperTextEl) : null; - - // Character counter - const characterCounterStrings = MDCTextFieldCharacterCounterFoundation.strings; - let characterCounterEl = this.root_.querySelector(characterCounterStrings.ROOT_SELECTOR); - // If character counter is not found in root element search in sibling element. - if (!characterCounterEl && hasHelperLine && nextElementSibling) { - characterCounterEl = nextElementSibling.querySelector(characterCounterStrings.ROOT_SELECTOR); - } - this.characterCounter_ = characterCounterEl ? characterCounterFactory(characterCounterEl) : null; - - this.leadingIcon_ = null; - this.trailingIcon_ = null; - const iconElements = this.root_.querySelectorAll(strings.ICON_SELECTOR); - if (iconElements.length > 0) { - if (iconElements.length > 1) { // Has both icons. - this.leadingIcon_ = iconFactory(iconElements[0]); - this.trailingIcon_ = iconFactory(iconElements[1]); - } else { - if (this.root_.classList.contains(cssClasses.WITH_LEADING_ICON)) { - this.leadingIcon_ = iconFactory(iconElements[0]); - } else { - this.trailingIcon_ = iconFactory(iconElements[0]); - } - } - } - - const isTextArea = this.root_.classList.contains(cssClasses.TEXTAREA); - const isOutlined = this.root_.classList.contains(cssClasses.OUTLINED); - this.ripple = (isTextArea || isOutlined) ? null : rippleFactory(this.root_, new MDCRippleFoundation({ - ...MDCRipple.createAdapter(this), - ...({ - // tslint:disable:object-literal-sort-keys - isSurfaceActive: () => ponyfill.matches(this.input_, ':active'), - registerInteractionHandler: (evtType, handler) => this.input_.addEventListener(evtType, handler), - deregisterInteractionHandler: (evtType, handler) => this.input_.removeEventListener(evtType, handler), - // tslint:enable:object-literal-sort-keys - }), - })); - } - - destroy() { - if (this.ripple) { - this.ripple.destroy(); - } - if (this.lineRipple_) { - this.lineRipple_.destroy(); - } - if (this.helperText_) { - this.helperText_.destroy(); - } - if (this.characterCounter_) { - this.characterCounter_.destroy(); - } - if (this.leadingIcon_) { - this.leadingIcon_.destroy(); - } - if (this.trailingIcon_) { - this.trailingIcon_.destroy(); - } - if (this.label_) { - this.label_.destroy(); - } - if (this.outline_) { - this.outline_.destroy(); - } - super.destroy(); - } - - /** - * Initializes the Text Field's internal state based on the environment's - * state. - */ - initialSyncWithDom() { - this.disabled = this.input_.disabled; - } - - get value(): string { - return this.foundation_.getValue(); - } - - /** - * @param value The value to set on the input. - */ - set value(value: string) { - this.foundation_.setValue(value); - } - - get disabled(): boolean { - return this.foundation_.isDisabled(); - } - - /** - * @param disabled Sets the Text Field disabled or enabled. - */ - set disabled(disabled: boolean) { - this.foundation_.setDisabled(disabled); - } - - get valid(): boolean { - return this.foundation_.isValid(); - } - - /** - * @param valid Sets the Text Field valid or invalid. - */ - set valid(valid: boolean) { - this.foundation_.setValid(valid); - } - - get required(): boolean { - return this.input_.required; - } - - /** - * @param required Sets the Text Field to required. - */ - set required(required: boolean) { - this.input_.required = required; - } - - get pattern(): string { - return this.input_.pattern; - } - - /** - * @param pattern Sets the input element's validation pattern. - */ - set pattern(pattern: string) { - this.input_.pattern = pattern; - } - - get minLength(): number { - return this.input_.minLength; - } - - /** - * @param minLength Sets the input element's minLength. - */ - set minLength(minLength: number) { - this.input_.minLength = minLength; - } - - get maxLength(): number { - return this.input_.maxLength; - } - - /** - * @param maxLength Sets the input element's maxLength. - */ - set maxLength(maxLength: number) { - // Chrome throws exception if maxLength is set to a value less than zero - if (maxLength < 0) { - this.input_.removeAttribute('maxLength'); - } else { - this.input_.maxLength = maxLength; - } - } - - get min(): string { - return this.input_.min; - } - - /** - * @param min Sets the input element's min. - */ - set min(min: string) { - this.input_.min = min; - } - - get max(): string { - return this.input_.max; - } - - /** - * @param max Sets the input element's max. - */ - set max(max: string) { - this.input_.max = max; - } - - get step(): string { - return this.input_.step; - } - - /** - * @param step Sets the input element's step. - */ - set step(step: string) { - this.input_.step = step; - } - - /** - * Sets the helper text element content. - */ - set helperTextContent(content: string) { - this.foundation_.setHelperTextContent(content); - } - - /** - * Sets the aria label of the leading icon. - */ - set leadingIconAriaLabel(label: string) { - this.foundation_.setLeadingIconAriaLabel(label); - } - - /** - * Sets the text content of the leading icon. - */ - set leadingIconContent(content: string) { - this.foundation_.setLeadingIconContent(content); - } - - /** - * Sets the aria label of the trailing icon. - */ - set trailingIconAriaLabel(label: string) { - this.foundation_.setTrailingIconAriaLabel(label); - } - - /** - * Sets the text content of the trailing icon. - */ - set trailingIconContent(content: string) { - this.foundation_.setTrailingIconContent(content); - } - - /** - * Enables or disables the use of native validation. Use this for custom validation. - * @param useNativeValidation Set this to false to ignore native input validation. - */ - set useNativeValidation(useNativeValidation: boolean) { - this.foundation_.setUseNativeValidation(useNativeValidation); - } - - /** - * Focuses the input element. - */ - focus() { - this.input_.focus(); - } - - /** - * Recomputes the outline SVG path for the outline element. - */ - layout() { - const openNotch = this.foundation_.shouldFloat; - this.foundation_.notchOutline(openNotch); - } - - getDefaultFoundation(): MDCTextFieldFoundation { - return new MDCTextFieldFoundation({ - ...({ - // tslint:disable:object-literal-sort-keys - addClass: (className) => this.root_.classList.add(className), - removeClass: (className) => this.root_.classList.remove(className), - hasClass: (className) => this.root_.classList.contains(className), - registerTextFieldInteractionHandler: (evtType, handler) => this.root_.addEventListener(evtType, handler), - deregisterTextFieldInteractionHandler: (evtType, handler) => this.root_.removeEventListener(evtType, handler), - registerValidationAttributeChangeHandler: (handler) => { - const getAttributesList = (mutationsList: MutationRecord[]): string[] => { - return mutationsList - .map((mutation) => mutation.attributeName) - .filter((attributeName) => attributeName) as string[]; - }; - const observer = new MutationObserver((mutationsList) => handler(getAttributesList(mutationsList))); - const config = {attributes: true}; - observer.observe(this.input_, config); - return observer; - }, - deregisterValidationAttributeChangeHandler: (observer) => observer.disconnect(), - isFocused: () => document.activeElement === this.input_, - // tslint:enable:object-literal-sort-keys - }), - ...this.getInputAdapterMethods_(), - ...this.getLabelAdapterMethods_(), - ...this.getLineRippleAdapterMethods_(), - ...this.getOutlineAdapterMethods_(), - }, this.getFoundationMap_()); - } - - private getLabelAdapterMethods_() { - return { - floatLabel: (shouldFloat: boolean) => this.label_ && this.label_.float(shouldFloat), - getLabelWidth: () => this.label_ ? this.label_.getWidth() : 0, - hasLabel: () => Boolean(this.label_), - shakeLabel: (shouldShake: boolean) => this.label_ && this.label_.shake(shouldShake), - }; - } - - private getLineRippleAdapterMethods_() { - return { - activateLineRipple: () => { - if (this.lineRipple_) { - this.lineRipple_.activate(); - } - }, - deactivateLineRipple: () => { - if (this.lineRipple_) { - this.lineRipple_.deactivate(); - } - }, - setLineRippleTransformOrigin: (normalizedX: number) => { - if (this.lineRipple_) { - this.lineRipple_.setRippleCenter(normalizedX); - } - }, - }; - } - - private getOutlineAdapterMethods_() { - return { - closeOutline: () => this.outline_ && this.outline_.closeNotch(), - hasOutline: () => Boolean(this.outline_), - notchOutline: (labelWidth: number) => this.outline_ && this.outline_.notch(labelWidth), - }; - } - - private getInputAdapterMethods_() { - // tslint:disable:object-literal-sort-keys - return { - registerInputInteractionHandler: (evtType: E, handler: SpecificEventListener) => { - this.input_.addEventListener(evtType, handler); - }, - deregisterInputInteractionHandler: (evtType: E, handler: SpecificEventListener) => { - this.input_.removeEventListener(evtType, handler); - }, - getNativeInput: () => this.input_, - }; - // tslint:enable:object-literal-sort-keys - } - - /** - * @return A map of all subcomponents to subfoundations. - */ - private getFoundationMap_(): Partial { - return { - characterCounter: this.characterCounter_ ? this.characterCounter_.foundation : undefined, - helperText: this.helperText_ ? this.helperText_.foundation : undefined, - leadingIcon: this.leadingIcon_ ? this.leadingIcon_.foundation : undefined, - trailingIcon: this.trailingIcon_ ? this.trailingIcon_.foundation : undefined, - }; - } -} - -export {MDCTextField as default, MDCTextField}; export * from './adapter'; +export * from './component'; export * from './foundation'; export * from './types'; export * from './character-counter/index'; diff --git a/packages/mdc-textfield/mdc-text-field.scss b/packages/mdc-textfield/mdc-text-field.scss index ad4339afddf..84212ce664a 100644 --- a/packages/mdc-textfield/mdc-text-field.scss +++ b/packages/mdc-textfield/mdc-text-field.scss @@ -71,6 +71,7 @@ box-sizing: border-box; height: $mdc-text-field-height; overflow: hidden; + /* @alternate */ will-change: opacity, transform, color; } diff --git a/packages/mdc-textfield/types.ts b/packages/mdc-textfield/types.ts index 7ac8a921d06..ccb261c2aad 100644 --- a/packages/mdc-textfield/types.ts +++ b/packages/mdc-textfield/types.ts @@ -21,33 +21,17 @@ * THE SOFTWARE. */ -import {MDCFloatingLabel} from '@material/floating-label/index'; -import {MDCLineRipple} from '@material/line-ripple/index'; -import {MDCNotchedOutline} from '@material/notched-outline/index'; -import {MDCRippleFoundation} from '@material/ripple/foundation'; -import {MDCRipple} from '@material/ripple/index'; import {MDCTextFieldCharacterCounterFoundation} from './character-counter/foundation'; -import {MDCTextFieldCharacterCounter} from './character-counter/index'; import {MDCTextFieldHelperTextFoundation} from './helper-text/foundation'; -import {MDCTextFieldHelperText} from './helper-text/index'; import {MDCTextFieldIconFoundation} from './icon/foundation'; -import {MDCTextFieldIcon} from './icon/index'; -export type NativeInputElement = Pick & { +export type MDCTextFieldNativeInputElement = Pick & { validity: Pick; }; -export interface FoundationMapType { +export interface MDCTextFieldFoundationMap { helperText: MDCTextFieldHelperTextFoundation; characterCounter: MDCTextFieldCharacterCounterFoundation; leadingIcon: MDCTextFieldIconFoundation; trailingIcon: MDCTextFieldIconFoundation; } - -export type RippleFactory = (el: Element, foundation: MDCRippleFoundation) => MDCRipple; -export type LineRippleFactory = (el: Element) => MDCLineRipple; -export type HelperTextFactory = (el: Element) => MDCTextFieldHelperText; -export type CharacterCounterFactory = (el: Element) => MDCTextFieldCharacterCounter; -export type IconFactory = (el: Element) => MDCTextFieldIcon; -export type LabelFactory = (el: Element) => MDCFloatingLabel; -export type OutlineFactory = (el: Element) => MDCNotchedOutline; diff --git a/packages/mdc-toolbar/README.md b/packages/mdc-toolbar/README.md index a92061ac956..2e837bdce57 100644 --- a/packages/mdc-toolbar/README.md +++ b/packages/mdc-toolbar/README.md @@ -421,7 +421,7 @@ Method Signature | Description Event Name | Event Data Structure | Description --- | --- | --- -`change` | `ToolbarEventDetail` | Emits the ratio of current flexible space to total flexible space height. So when it is minimized, ratio equals to 0 and when it is maximized, ratio equals to 1. See [types.ts](types.ts). +`change` | `MDCToolbarEventDetail` | Emits the ratio of current flexible space to total flexible space height. So when it is minimized, ratio equals to 0 and when it is maximized, ratio equals to 1. See [types.ts](types.ts). #### Adapter @@ -438,7 +438,7 @@ Method Signature | Description `getViewportScrollY() => number` | Gets the number of pixels that the content of body is scrolled upward `getOffsetHeight() => number` | Gets root element `mdc-toolbar` offsetHeight. `getFirstRowElementOffsetHeight() => number` | Gets first row element offsetHeight. -`notifyChange(evtData: ToolbarEventDetail) => void` | Broadcasts an event with the remaining ratio of flexible space. See [types.ts](types.ts). +`notifyChange(evtData: MDCToolbarEventDetail) => void` | Broadcasts an event with the remaining ratio of flexible space. See [types.ts](types.ts). `setStyle(property: string, value: number) => void` | Sets `mdc-toolbar` style property to provided value. `setStyleForTitleElement(property: string, value: number) => void` | Sets `mdc-toolbar__title` style property to provided value. `setStyleForFlexibleRowElement(property: string, value: number) => void` | Sets flexible row element style property to provided value. diff --git a/packages/mdc-toolbar/adapter.ts b/packages/mdc-toolbar/adapter.ts index 31577d32f99..84e161c6ab2 100644 --- a/packages/mdc-toolbar/adapter.ts +++ b/packages/mdc-toolbar/adapter.ts @@ -22,7 +22,7 @@ */ import {SpecificEventListener} from '@material/base/types'; -import {ToolbarEventDetail} from './types'; +import {MDCToolbarEventDetail} from './types'; /** * Defines the shape of the adapter expected by the foundation. @@ -43,11 +43,9 @@ export interface MDCToolbarAdapter { getViewportScrollY: () => number; getOffsetHeight: () => number; getFirstRowElementOffsetHeight: () => number; - notifyChange: (evtData: ToolbarEventDetail) => void; + notifyChange: (evtData: MDCToolbarEventDetail) => void; setStyle: (property: string, value: string) => void; setStyleForTitleElement: (property: string, value: string) => void; setStyleForFlexibleRowElement: (property: string, value: string) => void; setStyleForFixedAdjustElement: (property: string, value: string) => void; } - -export default MDCToolbarAdapter; diff --git a/packages/mdc-toolbar/component.ts b/packages/mdc-toolbar/component.ts index 32469208bcc..949a87ce6cb 100644 --- a/packages/mdc-toolbar/component.ts +++ b/packages/mdc-toolbar/component.ts @@ -22,10 +22,10 @@ */ import {MDCComponent} from '@material/base/component'; -import {MDCRipple} from '@material/ripple/index'; +import {MDCRipple} from '@material/ripple/component'; import {MDCToolbarAdapter} from './adapter'; import {MDCToolbarFoundation} from './foundation'; -import {ToolbarEventDetail} from './types'; +import {MDCToolbarEventDetail} from './types'; const {strings} = MDCToolbarFoundation; @@ -75,6 +75,8 @@ export class MDCToolbar extends MDCComponent { } getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. // tslint:disable:object-literal-sort-keys const adapter: MDCToolbarAdapter = { hasClass: (className) => this.root_.classList.contains(className), @@ -88,7 +90,7 @@ export class MDCToolbar extends MDCComponent { getViewportScrollY: () => window.pageYOffset, getOffsetHeight: () => this.root_.offsetHeight, getFirstRowElementOffsetHeight: () => this.firstRowElement_.offsetHeight, - notifyChange: (evtData) => this.emit(strings.CHANGE_EVENT, evtData), + notifyChange: (evtData) => this.emit(strings.CHANGE_EVENT, evtData), setStyle: (property, value) => this.root_.style.setProperty(property, value), setStyleForTitleElement: (property, value) => { if (this.titleElement_) { diff --git a/packages/mdc-toolbar/foundation.ts b/packages/mdc-toolbar/foundation.ts index b6fffc9950b..cacce561930 100644 --- a/packages/mdc-toolbar/foundation.ts +++ b/packages/mdc-toolbar/foundation.ts @@ -73,7 +73,7 @@ export class MDCToolbarFoundation extends MDCFoundation { return numbers; } - static get defaultAdapter() { + static get defaultAdapter(): MDCToolbarAdapter { // tslint:disable:object-literal-sort-keys return { hasClass: () => false, @@ -169,11 +169,11 @@ export class MDCToolbarFoundation extends MDCFoundation { this.calculations_.toolbarRowHeight = newToolbarRowHeight; this.calculations_.toolbarHeight = this.calculations_.toolbarRatio * this.calculations_.toolbarRowHeight; this.calculations_.flexibleExpansionHeight = - this.calculations_.flexibleExpansionRatio * this.calculations_.toolbarRowHeight; + this.calculations_.flexibleExpansionRatio * this.calculations_.toolbarRowHeight; this.calculations_.maxTranslateYDistance = - this.calculations_.maxTranslateYRatio * this.calculations_.toolbarRowHeight; + this.calculations_.maxTranslateYRatio * this.calculations_.toolbarRowHeight; this.calculations_.scrollThreshold = - this.calculations_.scrollThresholdRatio * this.calculations_.toolbarRowHeight; + this.calculations_.scrollThresholdRatio * this.calculations_.toolbarRowHeight; this.updateAdjustElementStyles(); this.updateToolbarStyles_(); } @@ -213,15 +213,15 @@ export class MDCToolbarFoundation extends MDCFoundation { this.calculations_.toolbarRatio = this.adapter_.getOffsetHeight() / toolbarRowHeight; this.calculations_.flexibleExpansionRatio = firstRowMaxRatio - 1; this.calculations_.maxTranslateYRatio = - this.isFixedLastRow_ ? this.calculations_.toolbarRatio - firstRowMaxRatio : 0; + this.isFixedLastRow_ ? this.calculations_.toolbarRatio - firstRowMaxRatio : 0; this.calculations_.scrollThresholdRatio = - (this.isFixedLastRow_ ? this.calculations_.toolbarRatio : firstRowMaxRatio) - 1; + (this.isFixedLastRow_ ? this.calculations_.toolbarRatio : firstRowMaxRatio) - 1; } private getRowHeight_() { const breakpoint = numbers.TOOLBAR_MOBILE_BREAKPOINT; return this.adapter_.getViewportWidth() < breakpoint ? - numbers.TOOLBAR_ROW_MOBILE_HEIGHT : numbers.TOOLBAR_ROW_HEIGHT; + numbers.TOOLBAR_ROW_MOBILE_HEIGHT : numbers.TOOLBAR_ROW_HEIGHT; } private updateToolbarFlexibleState_(flexibleExpansionRatio: number) { @@ -236,8 +236,8 @@ export class MDCToolbarFoundation extends MDCFoundation { private updateToolbarFixedState_(scrollTop: number) { const translateDistance = Math.max(0, Math.min( - scrollTop - this.calculations_.flexibleExpansionHeight, - this.calculations_.maxTranslateYDistance)); + scrollTop - this.calculations_.flexibleExpansionHeight, + this.calculations_.maxTranslateYDistance)); this.adapter_.setStyle('transform', `translateY(${-translateDistance}px)`); if (translateDistance === this.calculations_.maxTranslateYDistance) { @@ -251,7 +251,7 @@ export class MDCToolbarFoundation extends MDCFoundation { if (this.isFixed_) { const height = this.calculations_.flexibleExpansionHeight * flexibleExpansionRatio; this.adapter_.setStyleForFlexibleRowElement('height', - `${height + this.calculations_.toolbarRowHeight}px`); + `${height + this.calculations_.toolbarRowHeight}px`); } if (this.useFlexDefaultBehavior_) { this.updateElementStylesDefaultBehavior_(flexibleExpansionRatio); diff --git a/packages/mdc-toolbar/types.ts b/packages/mdc-toolbar/types.ts index 362100f5191..e6f307fb0b5 100644 --- a/packages/mdc-toolbar/types.ts +++ b/packages/mdc-toolbar/types.ts @@ -21,8 +21,12 @@ * THE SOFTWARE. */ -export interface ToolbarEventDetail { +export interface MDCToolbarEventDetail { flexibleExpansionRatio: number; } -export type ToolbarEvent = CustomEvent; +// Note: CustomEvent is not supported by Closure Compiler. + +export interface MDCToolbarEvent extends Event { + readonly detail: MDCToolbarEventDetail; +} diff --git a/packages/mdc-top-app-bar/adapter.ts b/packages/mdc-top-app-bar/adapter.ts index e1ab8eddd63..e3b9c62d0b8 100644 --- a/packages/mdc-top-app-bar/adapter.ts +++ b/packages/mdc-top-app-bar/adapter.ts @@ -30,7 +30,7 @@ import {EventType, SpecificEventListener} from '@material/base/types'; * for more details. * https://github.com/material-components/material-components-web/blob/master/docs/code/architecture.md */ -interface MDCTopAppBarAdapter { +export interface MDCTopAppBarAdapter { /** * Adds a class to the root Element. */ @@ -83,5 +83,3 @@ interface MDCTopAppBarAdapter { getTotalActionItems(): number; } - -export {MDCTopAppBarAdapter as default, MDCTopAppBarAdapter}; diff --git a/packages/mdc-top-app-bar/component.ts b/packages/mdc-top-app-bar/component.ts new file mode 100644 index 00000000000..863a9e8d258 --- /dev/null +++ b/packages/mdc-top-app-bar/component.ts @@ -0,0 +1,120 @@ +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import {MDCComponent} from '@material/base/component'; +import {MDCRipple, MDCRippleFactory} from '@material/ripple/component'; +import {MDCTopAppBarAdapter} from './adapter'; +import {cssClasses, strings} from './constants'; +import {MDCFixedTopAppBarFoundation} from './fixed/foundation'; +import {MDCTopAppBarBaseFoundation} from './foundation'; +import {MDCShortTopAppBarFoundation} from './short/foundation'; +import {MDCTopAppBarFoundation} from './standard/foundation'; + +export class MDCTopAppBar extends MDCComponent { + static attachTo(root: Element): MDCTopAppBar { + return new MDCTopAppBar(root); + } + + private navIcon_!: Element | null; + private iconRipples_!: MDCRipple[]; + private scrollTarget_!: EventTarget; + + initialize(rippleFactory: MDCRippleFactory = (el) => MDCRipple.attachTo(el)) { + this.navIcon_ = this.root_.querySelector(strings.NAVIGATION_ICON_SELECTOR); + + // Get all icons in the toolbar and instantiate the ripples + const icons: Element[] = [].slice.call(this.root_.querySelectorAll(strings.ACTION_ITEM_SELECTOR)); + if (this.navIcon_) { + icons.push(this.navIcon_); + } + + this.iconRipples_ = icons.map((icon) => { + const ripple = rippleFactory(icon); + ripple.unbounded = true; + return ripple; + }); + + this.scrollTarget_ = window; + } + + destroy() { + this.iconRipples_.forEach((iconRipple) => iconRipple.destroy()); + super.destroy(); + } + + setScrollTarget(target: EventTarget) { + // Remove scroll handler from the previous scroll target + this.foundation_.destroyScrollHandler(); + + this.scrollTarget_ = target; + + // Initialize scroll handler on the new scroll target + this.foundation_.initScrollHandler(); + } + + getDefaultFoundation() { + // DO NOT INLINE this variable. For backward compatibility, foundations take a Partial. + // To ensure we don't accidentally omit any methods, we need a separate, strongly typed adapter variable. + // tslint:disable:object-literal-sort-keys + const adapter: MDCTopAppBarAdapter = { + hasClass: (className) => this.root_.classList.contains(className), + addClass: (className) => this.root_.classList.add(className), + removeClass: (className) => this.root_.classList.remove(className), + setStyle: (property, value) => (this.root_ as HTMLElement).style.setProperty(property, value), + getTopAppBarHeight: () => this.root_.clientHeight, + registerNavigationIconInteractionHandler: (evtType, handler) => { + if (this.navIcon_) { + this.navIcon_.addEventListener(evtType, handler); + } + }, + deregisterNavigationIconInteractionHandler: (evtType, handler) => { + if (this.navIcon_) { + this.navIcon_.removeEventListener(evtType, handler); + } + }, + notifyNavigationIconClicked: () => this.emit(strings.NAVIGATION_EVENT, {}), + registerScrollHandler: (handler) => this.scrollTarget_.addEventListener('scroll', handler as EventListener), + deregisterScrollHandler: (handler) => this.scrollTarget_.removeEventListener('scroll', handler as EventListener), + registerResizeHandler: (handler) => window.addEventListener('resize', handler), + deregisterResizeHandler: (handler) => window.removeEventListener('resize', handler), + getViewportScrollY: () => { + const win = this.scrollTarget_ as Window; + const el = this.scrollTarget_ as Element; + return win.pageYOffset !== undefined ? win.pageYOffset : el.scrollTop; + }, + getTotalActionItems: () => this.root_.querySelectorAll(strings.ACTION_ITEM_SELECTOR).length, + }; + // tslint:enable:object-literal-sort-keys + + let foundation: MDCTopAppBarBaseFoundation; + if (this.root_.classList.contains(cssClasses.SHORT_CLASS)) { + foundation = new MDCShortTopAppBarFoundation(adapter); + } else if (this.root_.classList.contains(cssClasses.FIXED_CLASS)) { + foundation = new MDCFixedTopAppBarFoundation(adapter); + } else { + foundation = new MDCTopAppBarFoundation(adapter); + } + + return foundation; + } +} diff --git a/packages/mdc-top-app-bar/fixed/foundation.ts b/packages/mdc-top-app-bar/fixed/foundation.ts index 33a39bc0784..b49746805d0 100644 --- a/packages/mdc-top-app-bar/fixed/foundation.ts +++ b/packages/mdc-top-app-bar/fixed/foundation.ts @@ -25,7 +25,7 @@ import {MDCTopAppBarAdapter} from '../adapter'; import {cssClasses} from '../constants'; import {MDCTopAppBarFoundation} from '../standard/foundation'; -class MDCFixedTopAppBarFoundation extends MDCTopAppBarFoundation { +export class MDCFixedTopAppBarFoundation extends MDCTopAppBarFoundation { /** * State variable for the previous scroll iteration top app bar state */ @@ -58,4 +58,4 @@ class MDCFixedTopAppBarFoundation extends MDCTopAppBarFoundation { } } -export {MDCFixedTopAppBarFoundation as default, MDCFixedTopAppBarFoundation}; +export default MDCFixedTopAppBarFoundation; diff --git a/packages/mdc-top-app-bar/foundation.ts b/packages/mdc-top-app-bar/foundation.ts index 56c85c9d80b..0491bd5dd2f 100644 --- a/packages/mdc-top-app-bar/foundation.ts +++ b/packages/mdc-top-app-bar/foundation.ts @@ -26,7 +26,7 @@ import {SpecificEventListener} from '@material/base/types'; import {MDCTopAppBarAdapter} from './adapter'; import {cssClasses, numbers, strings} from './constants'; -class MDCTopAppBarBaseFoundation extends MDCFoundation { +export class MDCTopAppBarBaseFoundation extends MDCFoundation { static get strings() { return strings; } @@ -57,8 +57,8 @@ class MDCTopAppBarBaseFoundation extends MDCFoundation { deregisterScrollHandler: () => undefined, registerResizeHandler: () => undefined, deregisterResizeHandler: () => undefined, - getViewportScrollY: () => 0, - getTotalActionItems: () => 0, + getViewportScrollY: () => 0, + getTotalActionItems: () => 0, }; // tslint:enable:object-literal-sort-keys } @@ -111,4 +111,4 @@ class MDCTopAppBarBaseFoundation extends MDCFoundation { } } -export {MDCTopAppBarBaseFoundation as default, MDCTopAppBarBaseFoundation}; +export default MDCTopAppBarBaseFoundation; diff --git a/packages/mdc-top-app-bar/index.ts b/packages/mdc-top-app-bar/index.ts index 75907d67f76..36eeaa25292 100644 --- a/packages/mdc-top-app-bar/index.ts +++ b/packages/mdc-top-app-bar/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2018 Google Inc. + * Copyright 2019 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,107 +21,9 @@ * THE SOFTWARE. */ -import {MDCComponent} from '@material/base/component'; -import {MDCRipple} from '@material/ripple/index'; -import {MDCTopAppBarAdapter} from './adapter'; -import {cssClasses, strings} from './constants'; -import {MDCFixedTopAppBarFoundation} from './fixed/foundation'; -import {MDCTopAppBarBaseFoundation} from './foundation'; -import {MDCShortTopAppBarFoundation} from './short/foundation'; -import {MDCTopAppBarFoundation} from './standard/foundation'; - -export type MDCRippleFactory = (el: Element) => MDCRipple; - -class MDCTopAppBar extends MDCComponent { - static attachTo(root: Element): MDCTopAppBar { - return new MDCTopAppBar(root); - } - - private navIcon_!: Element | null; - private iconRipples_!: MDCRipple[]; - private scrollTarget_!: EventTarget; - - initialize(rippleFactory: MDCRippleFactory = (el) => MDCRipple.attachTo(el)) { - this.navIcon_ = this.root_.querySelector(strings.NAVIGATION_ICON_SELECTOR); - - // Get all icons in the toolbar and instantiate the ripples - const icons: Element[] = [].slice.call(this.root_.querySelectorAll(strings.ACTION_ITEM_SELECTOR)); - if (this.navIcon_) { - icons.push(this.navIcon_); - } - - this.iconRipples_ = icons.map((icon) => { - const ripple = rippleFactory(icon); - ripple.unbounded = true; - return ripple; - }); - - this.scrollTarget_ = window; - } - - destroy() { - this.iconRipples_.forEach((iconRipple) => iconRipple.destroy()); - super.destroy(); - } - - setScrollTarget(target: EventTarget) { - // Remove scroll handler from the previous scroll target - this.foundation_.destroyScrollHandler(); - - this.scrollTarget_ = target; - - // Initialize scroll handler on the new scroll target - this.foundation_.initScrollHandler(); - } - - getDefaultFoundation(): MDCTopAppBarBaseFoundation { - // tslint:disable:object-literal-sort-keys - const adapter: MDCTopAppBarAdapter = { - hasClass: (className) => this.root_.classList.contains(className), - addClass: (className) => this.root_.classList.add(className), - removeClass: (className) => this.root_.classList.remove(className), - setStyle: (property, value) => (this.root_ as HTMLElement).style.setProperty(property, value), - getTopAppBarHeight: () => this.root_.clientHeight, - registerNavigationIconInteractionHandler: (evtType, handler) => { - if (this.navIcon_) { - this.navIcon_.addEventListener(evtType, handler); - } - }, - deregisterNavigationIconInteractionHandler: (evtType, handler) => { - if (this.navIcon_) { - this.navIcon_.removeEventListener(evtType, handler); - } - }, - notifyNavigationIconClicked: () => this.emit(strings.NAVIGATION_EVENT, {}), - registerScrollHandler: (handler) => this.scrollTarget_.addEventListener('scroll', handler as EventListener), - deregisterScrollHandler: (handler) => this.scrollTarget_.removeEventListener('scroll', handler as EventListener), - registerResizeHandler: (handler) => window.addEventListener('resize', handler), - deregisterResizeHandler: (handler) => window.removeEventListener('resize', handler), - getViewportScrollY: () => { - const win = this.scrollTarget_ as Window; - const el = this.scrollTarget_ as Element; - return win.pageYOffset !== undefined ? win.pageYOffset : el.scrollTop; - }, - getTotalActionItems: () => this.root_.querySelectorAll(strings.ACTION_ITEM_SELECTOR).length, - }; - // tslint:enable:object-literal-sort-keys - - let foundation: MDCTopAppBarBaseFoundation; - if (this.root_.classList.contains(cssClasses.SHORT_CLASS)) { - foundation = new MDCShortTopAppBarFoundation(adapter); - } else if (this.root_.classList.contains(cssClasses.FIXED_CLASS)) { - foundation = new MDCFixedTopAppBarFoundation(adapter); - } else { - foundation = new MDCTopAppBarFoundation(adapter); - } - - return foundation; - } -} - -export {MDCTopAppBar as default, MDCTopAppBar}; +export * from './adapter'; +export * from './component'; +export * from './foundation'; export * from './fixed/foundation'; export * from './short/foundation'; export * from './standard/foundation'; -export * from './adapter'; -export * from './foundation'; diff --git a/packages/mdc-top-app-bar/short/foundation.ts b/packages/mdc-top-app-bar/short/foundation.ts index 6f8833faf55..ac325340f80 100644 --- a/packages/mdc-top-app-bar/short/foundation.ts +++ b/packages/mdc-top-app-bar/short/foundation.ts @@ -25,7 +25,7 @@ import {MDCTopAppBarAdapter} from '../adapter'; import {cssClasses} from '../constants'; import {MDCTopAppBarBaseFoundation} from '../foundation'; -class MDCShortTopAppBarFoundation extends MDCTopAppBarBaseFoundation { +export class MDCShortTopAppBarFoundation extends MDCTopAppBarBaseFoundation { /** * State variable for the current top app bar state */ @@ -74,4 +74,4 @@ class MDCShortTopAppBarFoundation extends MDCTopAppBarBaseFoundation { } } -export {MDCShortTopAppBarFoundation as default, MDCShortTopAppBarFoundation}; +export default MDCShortTopAppBarFoundation; diff --git a/packages/mdc-top-app-bar/standard/foundation.ts b/packages/mdc-top-app-bar/standard/foundation.ts index c05df38a54f..c7215024c14 100644 --- a/packages/mdc-top-app-bar/standard/foundation.ts +++ b/packages/mdc-top-app-bar/standard/foundation.ts @@ -27,7 +27,7 @@ import {MDCTopAppBarBaseFoundation} from '../foundation'; const INITIAL_VALUE = 0; -class MDCTopAppBarFoundation extends MDCTopAppBarBaseFoundation { +export class MDCTopAppBarFoundation extends MDCTopAppBarBaseFoundation { /** * Indicates if the top app bar was docked in the previous scroll handler iteration. */ @@ -193,4 +193,4 @@ class MDCTopAppBarFoundation extends MDCTopAppBarBaseFoundation { } } -export {MDCTopAppBarFoundation as default, MDCTopAppBarFoundation}; +export default MDCTopAppBarFoundation; diff --git a/scripts/rewrite-declaration-statements-for-typescript.js b/scripts/rewrite-declaration-statements-for-typescript.js deleted file mode 100644 index 0a867b08779..00000000000 --- a/scripts/rewrite-declaration-statements-for-typescript.js +++ /dev/null @@ -1,223 +0,0 @@ -/** - * @license - * Copyright 2017 Google Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -/** - * @fileoverview Rewrites JS and TS to match relative path format. That means - * * Rewrite import FooConstants from '@material/foo/constants' to import FooConstants from './../foo/index' - * * Rewrite import {FooFoundation} from '@material/foo' to import {FooFoundation} from './mdc-foo/index' - * - * - * This script rewrites import statements such that: - * - * ```js - * import [ from] '@material/$PKG[/files...]'; - * ``` - * becomes - * ```js - * import [ from] '..//$PKG[/files...]'; - * ``` - * The RELATIVE_PATH is the path relative from the current working file. - * - * This script also handles third-party dependencies, e.g. - * - * ```js - * import {thing1, thing2} from 'third-party-lib'; - * ``` - * - * becomes - * - * ```js - * import {thing1, thing2} from 'mdc.thirdparty.thirdPartyLib'; - * ``` - * - * and - * - * ```js - * import someDefaultExport from 'third-party-lib'; - * ``` - * - * becomes - * - * ```js - * import {default as someDefaultExport} from 'mdc.thirdparty.thirdPartyLib' - * ``` - */ - -const fs = require('fs'); -const path = require('path'); - -const {default: traverse} = require('babel-traverse'); -const parser = require('@babel/parser'); -const camelCase = require('camel-case'); -const glob = require('glob'); -const recast = require('recast'); -const resolve = require('resolve'); -const types = require('babel-types'); - -const THIRD_PARTY_PATH = 'mdc.thirdparty.'; - -const alreadyRewrittenImportPaths = new Set(); - -main(process.argv); - -function main(argv) { - if (argv.length < 3) { - console.error(`Usage: node ${path.basename(argv[1])} path/to/mdc-web/packages`); - process.exit(1); - } - - const packagesDir = path.resolve(process.argv[2]); - - /** @type {!Array} */ - const srcFileAbsolutePaths = glob.sync(`${packagesDir}/**/*.{js,ts}`, {ignore: ['**/node_modules/**']}); - - srcFileAbsolutePaths.forEach((srcFileAbsolutePath) => { - transformSrc(srcFileAbsolutePath); - }); - - logProgress(''); - console.log('\rTransform pass completed. ' + srcFileAbsolutePaths.length + ' files written.\n'); -} - -function getAstFromCodeString(codeString) { - return recast.parse(codeString, { - parser: { - parse: (code) => parser.parse(code, { - sourceType: 'module', - plugins: ['typescript', 'classProperties'], - }), - }, - }); -} - -/** - * @param {string} srcFileAbsolutePath - */ -function transformSrc(srcFileAbsolutePath) { - const src = fs.readFileSync(srcFileAbsolutePath, 'utf8'); - const ast = getAstFromCodeString(src); - - traverse(ast, { - ImportDeclaration(path) { - const packageStr = rewriteDeclarationSource(path.node, srcFileAbsolutePath); - const {value: sourceValue} = path.node.source; - - const hasThirdPartyTransformed = sourceValue.includes(THIRD_PARTY_PATH); - if (sourceValue !== packageStr && !hasThirdPartyTransformed) { - const importDeclaration = types.importDeclaration(path.node.specifiers, types.stringLiteral(packageStr)); - // Preserve comments above import statements, since this is most likely - // the license comment. - if (path.node.comments && path.node.comments.length > 0) { - for (let i = 0; i < path.node.comments.length; i++) { - const commentValue = path.node.comments[i].value; - importDeclaration.comments = importDeclaration.comments || []; - importDeclaration.comments.push({type: 'CommentBlock', value: commentValue}); - } - } - path.replaceWith(importDeclaration); - } - }, - }); - - const {code: outputCode} = recast.print(ast, { - objectCurlySpacing: false, - quote: 'single', - trailingComma: { - objects: false, - arrays: true, - parameters: false, - }, - }); - - fs.writeFileSync(srcFileAbsolutePath, outputCode, 'utf8'); - logProgress(`[rewrite] ${srcFileAbsolutePath}`); -} - -/** - * @param {!Object} astNode - * @param {string} srcFilePathAbsolute - * @return {string} New import path - */ -function rewriteDeclarationSource(astNode, srcFilePathAbsolute) { - const oldImportPath = astNode.source.value; - let newImportPath = oldImportPath; - const basedir = path.dirname(srcFilePathAbsolute); - - // TODO: This section of code will need to be revisited when a third party module is used internally. - // currently we haven't built internal dialog or drawer to actually use this rewrite correctly. - // Needed for focus-trap. - if (isThirdPartyModule(oldImportPath)) { - if (oldImportPath.indexOf('@material') > -1) { - return oldImportPath; - } - patchDefaultImportIfNeeded(astNode); - newImportPath = `${THIRD_PARTY_PATH}${camelCase(oldImportPath)}`; - return newImportPath; - } else { - if (alreadyRewrittenImportPaths.has(oldImportPath)) { - return oldImportPath; - } - const fileDir = resolve.sync(oldImportPath, { - basedir, - extensions: ['.ts', '.js'], - }); - const srcDirectory = path.dirname(srcFilePathAbsolute); - newImportPath = './' + getBazelFileNameOrPath( - path.relative(srcDirectory, fileDir) - .replace('.js', '') - .replace('.ts', '') - ); - alreadyRewrittenImportPaths.add(newImportPath); - return newImportPath; - } -} - -function patchDefaultImportIfNeeded(astNode) { - const defaultImportSpecifierIndex = - astNode.specifiers.findIndex(types.isImportDefaultSpecifier); - if (defaultImportSpecifierIndex >= 0) { - const defaultImportSpecifier = astNode.specifiers[defaultImportSpecifierIndex]; - const defaultPropImportSpecifier = types.importSpecifier(defaultImportSpecifier.local, types.identifier('default')); - astNode.specifiers[defaultImportSpecifierIndex] = defaultPropImportSpecifier; - } -} - -function isThirdPartyModule(importPath) { - // See: https://nodejs.org/api/modules.html#modules_all_together (step 3) - const wouldLoadAsFileOrDir = ['./', '/', '../'].some((s) => importPath.indexOf(s) === 0); - return !wouldLoadAsFileOrDir; -} - -function getBazelFileNameOrPath(ossFileNameOrPath) { - return ossFileNameOrPath - .replace(/mdc-?/g, '') - .replace(/selection-control/g, 'selection_control') - .replace(/character-counter/g, 'character_counter') - .replace(/helper-text/g, 'helper_text') - .replace(/-/g, '') - ; -} - -function logProgress(msg) { - console.log(msg); -} diff --git a/scripts/rewrite-sass-import-statements-for-closure.js b/scripts/rewrite-sass-import-statements-for-closure.js deleted file mode 100644 index 19b34962d3b..00000000000 --- a/scripts/rewrite-sass-import-statements-for-closure.js +++ /dev/null @@ -1,75 +0,0 @@ -/** - * @license - * Copyright 2017 Google Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -/** - * @fileoverview Rewrites import statements such that: - * - * ```js - * import [ from] '@material/$PKG[/files...]'; - * ``` - * becomes - * ```js - * import [ from] 'mdc-$PKG/'; - * ``` - * The RESOLVED_FILE_PATH is the file that node's module resolution algorithm would have resolved the import - * source to. - */ - -const fs = require('fs'); -const path = require('path'); - -const glob = require('glob'); - -main(process.argv); - -function main(argv) { - if (argv.length < 3) { - console.error('Missing root directory path'); - process.exit(1); - } - - const rootDir = path.resolve(process.argv[2]); - const srcFiles = glob.sync(`${rootDir}/**/*.scss`); - srcFiles.forEach((srcFile) => transform(srcFile, rootDir)); -} - -function transform(srcFile, rootDir) { - console.log(`[rewrite] ${srcFile}`); - let src = fs.readFileSync(srcFile, 'utf8'); - - src = src.replace(/@import "@material\/([^/]+)\/([^"]+)"/g, (match, modName, remainder) => { - const atMaterialReplacementPath = `${rootDir}/${modName}`; - const importSource = `${atMaterialReplacementPath}/${remainder}`; - - let resolvedImportSource = importSource; - const needsClosureModuleRootResolution = path.isAbsolute(importSource); - if (needsClosureModuleRootResolution) { - const pathToImport = importSource.replace('@material', rootDir); - resolvedImportSource = path.relative(path.dirname(srcFile), pathToImport); - } - - return `@import "${resolvedImportSource}"`; - }); - - fs.writeFileSync(srcFile, src, 'utf8'); -} diff --git a/scripts/sass-closure-rewriter.sh b/scripts/sass-closure-rewriter.sh deleted file mode 100755 index a1014ca3292..00000000000 --- a/scripts/sass-closure-rewriter.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash - -# Rewrites our .scss files to be compatible with closure-stylesheets -# in our internal Blaze infrastructure. - -## -# Copyright 2017 Google Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# - -set -e - -function log() { - echo -e "\033[36m[closure-rewriter]\033[0m" "$@" -} - -CLOSURE_TMP=.closure-tmp -CLOSURE_PKGDIR=$CLOSURE_TMP/packages - -log "Prepping packages for rewrite" - -rm -fr $CLOSURE_TMP/** -mkdir -p $CLOSURE_PKGDIR -PACKAGE_NAMES=$(ls packages) -for pkg in $PACKAGE_NAMES ; do - if [[ $pkg != *"mdc-"* ]]; then - continue - fi - cp -r "packages/$pkg" $CLOSURE_PKGDIR -done -rm -fr $CLOSURE_PKGDIR/**/{node_modules,dist} - -log "Rewriting all import statements to be closure compatible" -node scripts/rewrite-sass-import-statements-for-closure.js $CLOSURE_PKGDIR diff --git a/scripts/travis-env-vars.sh b/scripts/travis-env-vars.sh index 70ba782ba6b..0aaf7db6aff 100755 --- a/scripts/travis-env-vars.sh +++ b/scripts/travis-env-vars.sh @@ -78,7 +78,7 @@ print_all_changed_files if [[ "$TEST_SUITE" == 'unit' ]]; then # Only run unit tests if JS files changed - check_for_testable_files '^karma\.conf\.js$' '^packages/.+\.(js|ts)$' '^test/unit/.+\.js$' + check_for_testable_files '^karma\.conf\.js$' '^packages/.+\.(js|ts)$' '^test/unit/.+\.(js|ts)$' fi if [[ "$TEST_SUITE" == 'lint' ]]; then diff --git a/scripts/typescript-rewrite.sh b/scripts/typescript-rewrite.sh deleted file mode 100755 index 8cd041fb920..00000000000 --- a/scripts/typescript-rewrite.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash - -## -# Copyright 2017 Google Inc. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# - -set -e - -function log() { - echo -e "\033[36m[typescript-rewrite]\033[0m" "$@" -} - -TYPESCRIPT_TMP=.typescript-tmp -TYPESCRIPT_PKGDIR=$TYPESCRIPT_TMP/packages - -log "Prepping whitelisted packages for JS rewrite" - -rm -fr $TYPESCRIPT_TMP/** -mkdir -p $TYPESCRIPT_PKGDIR -for pkg in $(find ./packages -maxdepth 1 -type d); do - if [[ $pkg == *"mdc-"* ]]; then - cp -r $pkg $TYPESCRIPT_PKGDIR - fi -done -rm -fr $TYPESCRIPT_PKGDIR/**/{node_modules,dist} - -log "Rewriting all import statements to be internal typescript compatible" -node scripts/rewrite-declaration-statements-for-typescript.js $TYPESCRIPT_PKGDIR diff --git a/test/screenshot/.gitignore b/test/screenshot/.gitignore index 37715db4174..40189274d7a 100644 --- a/test/screenshot/.gitignore +++ b/test/screenshot/.gitignore @@ -1,2 +1,3 @@ index.html *.pid +out/ diff --git a/test/screenshot/infra/commands/test.js b/test/screenshot/infra/commands/test.js index f349a425060..1cf0061dc11 100644 --- a/test/screenshot/infra/commands/test.js +++ b/test/screenshot/infra/commands/test.js @@ -462,8 +462,6 @@ ${CliColor.bold.red('Skipping screenshot tests.')} */ logComparisonResults_(reportData) { this.logger_.foldStart('screenshot.diff_results', 'Diff results'); - this.logComparisonResultSet_('Skipped', reportData.screenshots.skipped_screenshot_list); - this.logComparisonResultSet_('Unchanged', reportData.screenshots.unchanged_screenshot_list); this.logComparisonResultSet_('Removed', reportData.screenshots.removed_screenshot_list); this.logComparisonResultSet_('Added', reportData.screenshots.added_screenshot_list); this.logComparisonResultSet_('Changed', reportData.screenshots.changed_screenshot_list); diff --git a/test/screenshot/infra/lib/diff-base-parser.js b/test/screenshot/infra/lib/diff-base-parser.js index 4d6c44b819c..a6a21f5294a 100644 --- a/test/screenshot/infra/lib/diff-base-parser.js +++ b/test/screenshot/infra/lib/diff-base-parser.js @@ -83,7 +83,7 @@ class DiffBaseParser { if (process.env.TRAVIS_BRANCH) { baseBranch = `origin/${process.env.TRAVIS_BRANCH}`; } else { - baseBranch = await this.gitHubApi_.getPullRequestBaseBranch(prNumber); + baseBranch = await this.gitHubApi_.getPullRequestBaseBranch(prNumber) || baseBranch; } } return this.parseDiffBase(baseBranch); diff --git a/test/screenshot/infra/lib/github-api.js b/test/screenshot/infra/lib/github-api.js index d731a693bc8..8fcc0ae01d9 100644 --- a/test/screenshot/infra/lib/github-api.js +++ b/test/screenshot/infra/lib/github-api.js @@ -21,8 +21,8 @@ * THE SOFTWARE. */ -const VError = require('verror'); -const octokit = require('@octokit/rest'); +const nodeUtil = require('util'); +const Octokit = require('@octokit/rest'); const GitRepo = require('./git-repo'); const getStackTrace = require('./stacktrace')('GitHubApi'); @@ -43,27 +43,24 @@ class GitHubApi { constructor() { this.gitRepo_ = new GitRepo(); - this.octokit_ = octokit(this.getAuthToken_()); this.isTravis_ = process.env.TRAVIS === 'true'; - this.isAuthenticated_ = false; + this.authToken_ = this.getAuth_(); + this.octokit_ = new Octokit(this.authToken_ ? {auth: this.authToken_} : undefined); } /** - * @return {({auth: string}|undefined)} + * @return {?string} * @private */ - getAuthToken_() { + getAuth_() { let token; try { token = require('../auth/github.json').api_key.personal_access_token; + return `token ${token}`; // https://github.com/octokit/rest.js/issues/1228 } catch (err) { - // Not running on Travis - return; + return null; // Not running on Travis } - - this.isAuthenticated_ = true; - return {auth: token}; } /** @@ -73,7 +70,7 @@ class GitHubApi { * @return {!Promise} */ async setPullRequestStatus({state, targetUrl, description = undefined}) { - if (!this.isTravis_ || !this.isAuthenticated_) { + if (!this.isTravis_ || !this.authToken_) { return null; } @@ -95,7 +92,8 @@ class GitHubApi { context: 'screenshot-test/butter-bot', }); } catch (err) { - throw new VError(err, `Failed to set commit status:\n${stackTrace}`); + this.error_('Failed to set commit status:', stackTrace, err); + return null; } } @@ -105,7 +103,7 @@ class GitHubApi { * @return {!Promise} */ async createPullRequestComment({prNumber, comment}) { - if (!this.isTravis_ || !this.isAuthenticated_) { + if (!this.isTravis_ || !this.authToken_) { return null; } @@ -120,7 +118,8 @@ class GitHubApi { body: comment, }); } catch (err) { - throw new VError(err, `Failed to create comment on PR #${prNumber}:\n${stackTrace}`); + this.error_(`Failed to create comment on PR #${prNumber}:`, stackTrace, err); + return null; } } @@ -142,7 +141,8 @@ class GitHubApi { per_page: 100, }); } catch (err) { - throw new VError(err, `Failed to get pull request number for branch "${branch}":\n${stackTrace}`); + this.error_(`Failed to get pull request number for branch "${branch}":`, stackTrace, err); + return null; } const filteredPRs = allPrsResponse.data.filter((pr) => pr.head.ref === branch); @@ -153,7 +153,7 @@ class GitHubApi { /** * @param {number} prNumber - * @return {!Promise} + * @return {!Promise} */ async getPullRequestBaseBranch(prNumber) { let prResponse; @@ -167,16 +167,32 @@ class GitHubApi { number: prNumber, }); } catch (err) { - throw new VError(err, `Failed to get the base branch for PR #${prNumber}:\n${stackTrace}`); + this.error_(`Failed to get the base branch for PR #${prNumber}:`, stackTrace, err); + return null; } if (!prResponse.data) { const serialized = JSON.stringify(prResponse, null, 2); - throw new Error(`Unable to fetch data for GitHub PR #${prNumber}:\n${serialized}`); + this.error_(`Unable to fetch data for GitHub PR #${prNumber}:`, serialized); + return null; } return `origin/${prResponse.data.base.ref}`; } + + /** + * @param {string} message + * @param {string} stackTrace + * @param {(Error|string)=} unsafeErr + * @private + */ + error_(message, stackTrace, unsafeErr = '') { + let redactedError = nodeUtil.inspect(unsafeErr, /* showHidden */ false, /* depth */ 5); + while (this.authToken_ && redactedError.indexOf(this.authToken_) > -1) { + redactedError = redactedError.replace(this.authToken_, '[REDACTED]'); + } + console.warn(`${message}:\n${stackTrace}\n\n`, redactedError); + } } module.exports = GitHubApi; diff --git a/test/screenshot/infra/lib/local-storage.js b/test/screenshot/infra/lib/local-storage.js index 528c5559b83..644fa01b659 100644 --- a/test/screenshot/infra/lib/local-storage.js +++ b/test/screenshot/infra/lib/local-storage.js @@ -255,7 +255,7 @@ class LocalStorage { this.logger_.debug(`Finding all files in "${cwd}"...`); /** @type {!Array} */ - const allFilePaths = glob.sync('**/*', {cwd, nodir: true}); + const allFilePaths = glob.sync('**/*', {cwd, nodir: true, ignore: ['**/out/**/*.d.ts', '**/*.map']}); this.logger_.debug(`Found ${allFilePaths.length.toLocaleString()} files in "${cwd}"!`); diff --git a/test/screenshot/infra/lib/report-builder.js b/test/screenshot/infra/lib/report-builder.js index 107b908e120..aac2caee9d2 100644 --- a/test/screenshot/infra/lib/report-builder.js +++ b/test/screenshot/infra/lib/report-builder.js @@ -996,20 +996,7 @@ class ReportBuilder { logRunParameters_(verb, screenshots) { const count = screenshots.length; const plural = count === 1 ? '' : 's:'; - console.log(`${verb} ${count} screenshot${plural}`); - if (count > 0) { - for (const screenshot of screenshots) { - const htmlFile = screenshot.actual_html_file || screenshot.expected_html_file; - const publicUrl = this.analytics_.getUrl({ - url: htmlFile.public_url, - source: 'cli', - medium: 'inventory', - }); - console.log(` - ${this.cli_.colorizeUrl(publicUrl)} > ${screenshot.user_agent.alias}`); - } - } - console.log(); } } diff --git a/test/screenshot/spec/mdc-textfield/fixture.scss b/test/screenshot/spec/mdc-textfield/fixture.scss index 30d316ec356..fd7d27576b1 100644 --- a/test/screenshot/spec/mdc-textfield/fixture.scss +++ b/test/screenshot/spec/mdc-textfield/fixture.scss @@ -56,7 +56,7 @@ // Work around MS Edge rendering bug. // TODO(acdvorak): Create and link to GitHub issue -@supports (-ms-ime-align:auto) { +@supports (-ms-ime-align: auto) { .test-text-field__input { transform: translateX(.1px); } diff --git a/test/unit/mdc-chips/mdc-chip-set.foundation.test.js b/test/unit/mdc-chips/mdc-chip-set.foundation.test.js index cb12f637cbf..be0c98a6774 100644 --- a/test/unit/mdc-chips/mdc-chip-set.foundation.test.js +++ b/test/unit/mdc-chips/mdc-chip-set.foundation.test.js @@ -25,7 +25,7 @@ import {assert} from 'chai'; import td from 'testdouble'; import {verifyDefaultAdapter} from '../helpers/foundation'; -import MDCChipSetFoundation from '../../../packages/mdc-chips/chip-set/foundation'; +import {MDCChipSetFoundation} from '../../../packages/mdc-chips/chip-set/foundation'; const {cssClasses} = MDCChipSetFoundation; diff --git a/test/unit/mdc-dialog/foundation.test.js b/test/unit/mdc-dialog/foundation.test.js index d9b983357f5..56040beae8e 100644 --- a/test/unit/mdc-dialog/foundation.test.js +++ b/test/unit/mdc-dialog/foundation.test.js @@ -29,7 +29,7 @@ import {verifyDefaultAdapter} from '../helpers/foundation'; import {cssClasses, strings, numbers} from '../../../packages/mdc-dialog/constants'; import {install as installClock} from '../helpers/clock'; -import MDCDialogFoundation from '../../../packages/mdc-dialog/foundation'; +import {MDCDialogFoundation} from '../../../packages/mdc-dialog/foundation'; const ENTER_EVENTS = [ {type: 'keydown', key: 'Enter', target: {}}, diff --git a/test/unit/mdc-drawer/modal.foundation.test.js b/test/unit/mdc-drawer/modal.foundation.test.js index 4d2d5fbace7..054ee7e11dc 100644 --- a/test/unit/mdc-drawer/modal.foundation.test.js +++ b/test/unit/mdc-drawer/modal.foundation.test.js @@ -22,7 +22,7 @@ */ import td from 'testdouble'; -import MDCModalDrawerFoundation from '../../../packages/mdc-drawer/modal/foundation'; +import {MDCModalDrawerFoundation} from '../../../packages/mdc-drawer/modal/foundation'; suite('MDCModalDrawerFoundation'); diff --git a/test/unit/mdc-floating-label/mdc-floating-label-foundation.test.js b/test/unit/mdc-floating-label/mdc-floating-label-foundation.test.js index 853a524df14..835858e9aa9 100644 --- a/test/unit/mdc-floating-label/mdc-floating-label-foundation.test.js +++ b/test/unit/mdc-floating-label/mdc-floating-label-foundation.test.js @@ -26,7 +26,7 @@ import td from 'testdouble'; import {captureHandlers, verifyDefaultAdapter} from '../helpers/foundation'; import {setupFoundationTest} from '../helpers/setup'; -import MDCFloatingLabelFoundation from '../../../packages/mdc-floating-label/foundation'; +import {MDCFloatingLabelFoundation} from '../../../packages/mdc-floating-label/foundation'; const {cssClasses} = MDCFloatingLabelFoundation; diff --git a/test/unit/mdc-grid-list/foundation.test.js b/test/unit/mdc-grid-list/foundation.test.js index 8ff479159cb..fce4421a5c3 100644 --- a/test/unit/mdc-grid-list/foundation.test.js +++ b/test/unit/mdc-grid-list/foundation.test.js @@ -27,7 +27,7 @@ import td from 'testdouble'; import {install as installClock} from '../helpers/clock'; import {setupFoundationTest} from '../helpers/setup'; import {verifyDefaultAdapter} from '../helpers/foundation'; -import MDCGridListFoundation from '../../../packages/mdc-grid-list/foundation'; +import {MDCGridListFoundation} from '../../../packages/mdc-grid-list/foundation'; suite('MDCGridListFoundation'); diff --git a/test/unit/mdc-line-ripple/mdc-line-ripple-foundation.test.js b/test/unit/mdc-line-ripple/mdc-line-ripple-foundation.test.js index d22aa0efa09..f2c9ad228f5 100644 --- a/test/unit/mdc-line-ripple/mdc-line-ripple-foundation.test.js +++ b/test/unit/mdc-line-ripple/mdc-line-ripple-foundation.test.js @@ -26,7 +26,7 @@ import td from 'testdouble'; import {verifyDefaultAdapter} from '../helpers/foundation'; import {setupFoundationTest} from '../helpers/setup'; -import MDCLineRippleFoundation from '../../../packages/mdc-line-ripple/foundation'; +import {MDCLineRippleFoundation} from '../../../packages/mdc-line-ripple/foundation'; const {cssClasses} = MDCLineRippleFoundation; diff --git a/test/unit/mdc-menu-surface/menu-surface.foundation.test.js b/test/unit/mdc-menu-surface/menu-surface.foundation.test.js index d1fd9a2ea21..d050b441366 100644 --- a/test/unit/mdc-menu-surface/menu-surface.foundation.test.js +++ b/test/unit/mdc-menu-surface/menu-surface.foundation.test.js @@ -61,7 +61,7 @@ const wideTopLeft = {height: 20, width: 150, top: 20, bottom: 40, left: 20, righ * @param {!ClientRect} anchorDimensions Approximate viewport corner where anchor is located. * @param {boolean=} isRtl Indicates whether layout is RTL. * @param {number=} menuSurfaceHeight Optional height of the menu surface. - * @param {!MenuPoint=} scrollValue Optional scroll values of the page. + * @param {!MDCMenuPoint=} scrollValue Optional scroll values of the page. */ function initAnchorLayout(mockAdapter, anchorDimensions, isRtl = false, menuSurfaceHeight = 200, scrollValue = {x: 0, y: 0}) { diff --git a/test/unit/mdc-notched-outline/foundation.test.js b/test/unit/mdc-notched-outline/foundation.test.js index e9bf0ca63de..bfc61873f5c 100644 --- a/test/unit/mdc-notched-outline/foundation.test.js +++ b/test/unit/mdc-notched-outline/foundation.test.js @@ -26,7 +26,7 @@ import td from 'testdouble'; import {verifyDefaultAdapter} from '../helpers/foundation'; import {setupFoundationTest} from '../helpers/setup'; -import MDCNotchedOutlineFoundation from '../../../packages/mdc-notched-outline/foundation'; +import {MDCNotchedOutlineFoundation} from '../../../packages/mdc-notched-outline/foundation'; const {cssClasses, numbers, strings} = MDCNotchedOutlineFoundation; diff --git a/test/unit/mdc-select/foundation.test.js b/test/unit/mdc-select/foundation.test.js index 8829ee886ac..a0414ac8148 100644 --- a/test/unit/mdc-select/foundation.test.js +++ b/test/unit/mdc-select/foundation.test.js @@ -26,7 +26,7 @@ import td from 'testdouble'; import {verifyDefaultAdapter} from '../helpers/foundation'; -import MDCSelectFoundation from '../../../packages/mdc-select/foundation'; +import {MDCSelectFoundation} from '../../../packages/mdc-select/foundation'; import {cssClasses, strings, numbers} from '../../../packages/mdc-select/constants'; const LABEL_WIDTH = 100; diff --git a/test/unit/mdc-select/mdc-select-enhanced.test.js b/test/unit/mdc-select/mdc-select-enhanced.test.js index f111522720c..f80dd52c96b 100644 --- a/test/unit/mdc-select/mdc-select-enhanced.test.js +++ b/test/unit/mdc-select/mdc-select-enhanced.test.js @@ -34,7 +34,7 @@ import {cssClasses, strings} from '../../../packages/mdc-select/constants'; import {MDCNotchedOutline} from '../../../packages/mdc-notched-outline/index'; import {MDCMenu, MDCMenuFoundation} from '../../../packages/mdc-menu/index'; import {MDCMenuSurfaceFoundation} from '../../../packages/mdc-menu-surface/index'; -import MDCSelectFoundation from '../../../packages/mdc-select/foundation'; +import {MDCSelectFoundation} from '../../../packages/mdc-select/foundation'; import {MDCListFoundation} from '../../../packages/mdc-list/foundation'; import {MDCSelectIcon} from '../../../packages/mdc-select/icon'; diff --git a/test/unit/mdc-select/mdc-select-helper-text-foundation.test.js b/test/unit/mdc-select/mdc-select-helper-text-foundation.test.js index a1458c9858a..b19a9eeaba4 100644 --- a/test/unit/mdc-select/mdc-select-helper-text-foundation.test.js +++ b/test/unit/mdc-select/mdc-select-helper-text-foundation.test.js @@ -26,7 +26,7 @@ import td from 'testdouble'; import {verifyDefaultAdapter} from '../helpers/foundation'; import {setupFoundationTest} from '../helpers/setup'; -import MDCSelectHelperTextFoundation from '../../../packages/mdc-select/helper-text/foundation'; +import {MDCSelectHelperTextFoundation} from '../../../packages/mdc-select/helper-text/foundation'; const {cssClasses, strings} = MDCSelectHelperTextFoundation; diff --git a/test/unit/mdc-select/mdc-select-icon-foundation.test.js b/test/unit/mdc-select/mdc-select-icon-foundation.test.js index 67b4bd8f631..0b53f671b8b 100644 --- a/test/unit/mdc-select/mdc-select-icon-foundation.test.js +++ b/test/unit/mdc-select/mdc-select-icon-foundation.test.js @@ -49,7 +49,7 @@ import td from 'testdouble'; import {verifyDefaultAdapter} from '../helpers/foundation'; import {setupFoundationTest} from '../helpers/setup'; -import MDCSelectIconFoundation from '../../../packages/mdc-select/icon/foundation'; +import {MDCSelectIconFoundation} from '../../../packages/mdc-select/icon/foundation'; import {strings} from '../../../packages/mdc-select/icon/constants'; suite('MDCSelectIconFoundation'); diff --git a/test/unit/mdc-slider/foundation.test.js b/test/unit/mdc-slider/foundation.test.js index 8af09fb9b2f..15074627ba6 100644 --- a/test/unit/mdc-slider/foundation.test.js +++ b/test/unit/mdc-slider/foundation.test.js @@ -30,7 +30,7 @@ import {install as installClock} from '../helpers/clock'; import {setupFoundationTest} from '../helpers/setup'; import {cssClasses} from '../../../packages/mdc-slider/constants'; -import MDCSliderFoundation from '../../../packages/mdc-slider/foundation'; +import {MDCSliderFoundation} from '../../../packages/mdc-slider/foundation'; suite('MDCSliderFoundation'); diff --git a/test/unit/mdc-slider/helpers.js b/test/unit/mdc-slider/helpers.js index ac8ba553ae3..ea2199f0352 100644 --- a/test/unit/mdc-slider/helpers.js +++ b/test/unit/mdc-slider/helpers.js @@ -26,7 +26,7 @@ import {captureHandlers} from '../helpers/foundation'; import {install as installClock} from '../helpers/clock'; import {setupFoundationTest} from '../helpers/setup'; -import MDCSliderFoundation from '../../../packages/mdc-slider/foundation'; +import {MDCSliderFoundation} from '../../../packages/mdc-slider/foundation'; export const TRANSFORM_PROP = getCorrectPropertyName(window, 'transform'); diff --git a/test/unit/mdc-snackbar/foundation.test.js b/test/unit/mdc-snackbar/foundation.test.js index 8a9f5dee1c7..bf28ecf17da 100644 --- a/test/unit/mdc-snackbar/foundation.test.js +++ b/test/unit/mdc-snackbar/foundation.test.js @@ -29,7 +29,7 @@ import {verifyDefaultAdapter} from '../helpers/foundation'; import {cssClasses, strings, numbers} from '../../../packages/mdc-snackbar/constants'; import {install as installClock} from '../helpers/clock'; -import MDCSnackbarFoundation from '../../../packages/mdc-snackbar/foundation'; +import {MDCSnackbarFoundation} from '../../../packages/mdc-snackbar/foundation'; suite('MDCSnackbarFoundation'); diff --git a/test/unit/mdc-tab-bar/foundation.test.js b/test/unit/mdc-tab-bar/foundation.test.js index 5b99d5ca70e..902b1b30cac 100644 --- a/test/unit/mdc-tab-bar/foundation.test.js +++ b/test/unit/mdc-tab-bar/foundation.test.js @@ -26,7 +26,7 @@ import td from 'testdouble'; import {verifyDefaultAdapter} from '../helpers/foundation'; import {setupFoundationTest} from '../helpers/setup'; -import MDCTabBarFoundation from '../../../packages/mdc-tab-bar/foundation'; +import {MDCTabBarFoundation} from '../../../packages/mdc-tab-bar/foundation'; suite('MDCTabBarFoundation'); diff --git a/test/unit/mdc-tab-scroller/foundation.test.js b/test/unit/mdc-tab-scroller/foundation.test.js index 37aa4165b15..c7cf51f937b 100644 --- a/test/unit/mdc-tab-scroller/foundation.test.js +++ b/test/unit/mdc-tab-scroller/foundation.test.js @@ -27,10 +27,10 @@ import td from 'testdouble'; import {verifyDefaultAdapter} from '../helpers/foundation'; import {install as installClock} from '../helpers/clock'; import {setupFoundationTest} from '../helpers/setup'; -import MDCTabScrollerFoundation from '../../../packages/mdc-tab-scroller/foundation'; -import MDCTabScrollerRTLDefault from '../../../packages/mdc-tab-scroller/rtl-default-scroller'; -import MDCTabScrollerRTLNegative from '../../../packages/mdc-tab-scroller/rtl-negative-scroller'; -import MDCTabScrollerRTLReverse from '../../../packages/mdc-tab-scroller/rtl-reverse-scroller'; +import {MDCTabScrollerFoundation} from '../../../packages/mdc-tab-scroller/foundation'; +import {MDCTabScrollerRTLDefault} from '../../../packages/mdc-tab-scroller/rtl-default-scroller'; +import {MDCTabScrollerRTLNegative} from '../../../packages/mdc-tab-scroller/rtl-negative-scroller'; +import {MDCTabScrollerRTLReverse} from '../../../packages/mdc-tab-scroller/rtl-reverse-scroller'; suite('MDCTabScrollerFoundation'); diff --git a/test/unit/mdc-tab-scroller/mdc-tab-scroller.test.js b/test/unit/mdc-tab-scroller/mdc-tab-scroller.test.js index 459bb10e13c..08682cc74fc 100644 --- a/test/unit/mdc-tab-scroller/mdc-tab-scroller.test.js +++ b/test/unit/mdc-tab-scroller/mdc-tab-scroller.test.js @@ -32,7 +32,7 @@ import { util, } from '../../../packages/mdc-tab-scroller/index'; -import MDCTabScrollerRTL from '../../../packages/mdc-tab-scroller/rtl-scroller'; +import {MDCTabScrollerRTL} from '../../../packages/mdc-tab-scroller/rtl-scroller'; const getFixture = () => bel`
diff --git a/test/unit/mdc-tab-scroller/rtl-default-scroller.test.js b/test/unit/mdc-tab-scroller/rtl-default-scroller.test.js index 5fa9cb37665..bc17d22f467 100644 --- a/test/unit/mdc-tab-scroller/rtl-default-scroller.test.js +++ b/test/unit/mdc-tab-scroller/rtl-default-scroller.test.js @@ -25,8 +25,8 @@ import {assert} from 'chai'; import td from 'testdouble'; import {setupFoundationTest} from '../helpers/setup'; -import MDCTabScrollerFoundation from '../../../packages/mdc-tab-scroller/foundation'; -import MDCTabScrollerRTLDefault from '../../../packages/mdc-tab-scroller/rtl-default-scroller'; +import {MDCTabScrollerFoundation} from '../../../packages/mdc-tab-scroller/foundation'; +import {MDCTabScrollerRTLDefault} from '../../../packages/mdc-tab-scroller/rtl-default-scroller'; suite('MDCTabScrollerRTLDefault'); diff --git a/test/unit/mdc-tab-scroller/rtl-negative-scroller.test.js b/test/unit/mdc-tab-scroller/rtl-negative-scroller.test.js index 99254b7bd0b..63d829bf225 100644 --- a/test/unit/mdc-tab-scroller/rtl-negative-scroller.test.js +++ b/test/unit/mdc-tab-scroller/rtl-negative-scroller.test.js @@ -25,8 +25,8 @@ import {assert} from 'chai'; import td from 'testdouble'; import {setupFoundationTest} from '../helpers/setup'; -import MDCTabScrollerFoundation from '../../../packages/mdc-tab-scroller/foundation'; -import MDCTabScrollerRTLNegative from '../../../packages/mdc-tab-scroller/rtl-negative-scroller'; +import {MDCTabScrollerFoundation} from '../../../packages/mdc-tab-scroller/foundation'; +import {MDCTabScrollerRTLNegative} from '../../../packages/mdc-tab-scroller/rtl-negative-scroller'; suite('MDCTabScrollerRTLNegative'); diff --git a/test/unit/mdc-tab-scroller/rtl-reverse-scroller.test.js b/test/unit/mdc-tab-scroller/rtl-reverse-scroller.test.js index d0ac9d60c6a..b39b1d8ea7b 100644 --- a/test/unit/mdc-tab-scroller/rtl-reverse-scroller.test.js +++ b/test/unit/mdc-tab-scroller/rtl-reverse-scroller.test.js @@ -25,8 +25,8 @@ import {assert} from 'chai'; import td from 'testdouble'; import {setupFoundationTest} from '../helpers/setup'; -import MDCTabScrollerFoundation from '../../../packages/mdc-tab-scroller/foundation'; -import MDCTabScrollerRTLReverse from '../../../packages/mdc-tab-scroller/rtl-reverse-scroller'; +import {MDCTabScrollerFoundation} from '../../../packages/mdc-tab-scroller/foundation'; +import {MDCTabScrollerRTLReverse} from '../../../packages/mdc-tab-scroller/rtl-reverse-scroller'; suite('MDCTabScrollerRTLReverse'); diff --git a/test/unit/mdc-tab/foundation.test.js b/test/unit/mdc-tab/foundation.test.js index a36d50b3624..d3786afc50f 100644 --- a/test/unit/mdc-tab/foundation.test.js +++ b/test/unit/mdc-tab/foundation.test.js @@ -26,7 +26,7 @@ import td from 'testdouble'; import {verifyDefaultAdapter} from '../helpers/foundation'; import {setupFoundationTest} from '../helpers/setup'; -import MDCTabFoundation from '../../../packages/mdc-tab/foundation'; +import {MDCTabFoundation} from '../../../packages/mdc-tab/foundation'; suite('MDCTabFoundation'); diff --git a/test/unit/mdc-tabs/mdc-tab-bar-foundation.test.js b/test/unit/mdc-tabs/mdc-tab-bar-foundation.test.js index 62406d63df9..c54418f2303 100644 --- a/test/unit/mdc-tabs/mdc-tab-bar-foundation.test.js +++ b/test/unit/mdc-tabs/mdc-tab-bar-foundation.test.js @@ -28,7 +28,7 @@ import {setupFoundationTest} from '../helpers/setup'; import {verifyDefaultAdapter, captureHandlers} from '../helpers/foundation'; import {install as installClock} from '../helpers/clock'; -import MDCTabBarFoundation from '../../../packages/mdc-tabs/tab-bar/foundation'; +import {MDCTabBarFoundation} from '../../../packages/mdc-tabs/tab-bar/foundation'; suite('MDCTabBarFoundation'); diff --git a/test/unit/mdc-tabs/mdc-tab-bar-scroller-foundation.test.js b/test/unit/mdc-tabs/mdc-tab-bar-scroller-foundation.test.js index 78286d19027..bed6621dfee 100644 --- a/test/unit/mdc-tabs/mdc-tab-bar-scroller-foundation.test.js +++ b/test/unit/mdc-tabs/mdc-tab-bar-scroller-foundation.test.js @@ -28,7 +28,7 @@ import {setupFoundationTest} from '../helpers/setup'; import {verifyDefaultAdapter} from '../helpers/foundation'; import {install as installClock} from '../helpers/clock'; -import MDCTabBarScrollerFoundation from '../../../packages/mdc-tabs/tab-bar-scroller/foundation'; +import {MDCTabBarScrollerFoundation} from '../../../packages/mdc-tabs/tab-bar-scroller/foundation'; suite('MDCTabBarScrollerFoundation'); diff --git a/test/unit/mdc-tabs/mdc-tab-foundation.test.js b/test/unit/mdc-tabs/mdc-tab-foundation.test.js index 7389f7bd2a8..4bc5de24347 100644 --- a/test/unit/mdc-tabs/mdc-tab-foundation.test.js +++ b/test/unit/mdc-tabs/mdc-tab-foundation.test.js @@ -28,7 +28,7 @@ import {setupFoundationTest} from '../helpers/setup'; import {verifyDefaultAdapter, captureHandlers} from '../helpers/foundation'; import {cssClasses} from '../../../packages/mdc-tabs/tab/constants'; -import MDCTabFoundation from '../../../packages/mdc-tabs/tab/foundation'; +import {MDCTabFoundation} from '../../../packages/mdc-tabs/tab/foundation'; suite('MDCTabFoundation'); diff --git a/test/unit/mdc-textfield/foundation.test.js b/test/unit/mdc-textfield/foundation.test.js index 6f0bfab0c14..ae118992d37 100644 --- a/test/unit/mdc-textfield/foundation.test.js +++ b/test/unit/mdc-textfield/foundation.test.js @@ -25,7 +25,7 @@ import {assert} from 'chai'; import td from 'testdouble'; import {verifyDefaultAdapter} from '../helpers/foundation'; -import MDCTextFieldFoundation from '../../../packages/mdc-textfield/foundation'; +import {MDCTextFieldFoundation} from '../../../packages/mdc-textfield/foundation'; const LABEL_WIDTH = 100; const {cssClasses, numbers} = MDCTextFieldFoundation; diff --git a/test/unit/mdc-textfield/mdc-text-field-character-counter-foundation.test.js b/test/unit/mdc-textfield/mdc-text-field-character-counter-foundation.test.js index 2561f42b99e..625dba7f45e 100644 --- a/test/unit/mdc-textfield/mdc-text-field-character-counter-foundation.test.js +++ b/test/unit/mdc-textfield/mdc-text-field-character-counter-foundation.test.js @@ -26,7 +26,7 @@ import td from 'testdouble'; import {verifyDefaultAdapter} from '../helpers/foundation'; import {setupFoundationTest} from '../helpers/setup'; -import MDCTextFieldCharacterCounterFoundation from '../../../packages/mdc-textfield/character-counter/foundation'; +import {MDCTextFieldCharacterCounterFoundation} from '../../../packages/mdc-textfield/character-counter/foundation'; suite('MDCTextFieldCharacterCounterFoundation'); diff --git a/test/unit/mdc-textfield/mdc-text-field-helper-text-foundation.test.js b/test/unit/mdc-textfield/mdc-text-field-helper-text-foundation.test.js index 1bb4d634f0b..92c9a1821ad 100644 --- a/test/unit/mdc-textfield/mdc-text-field-helper-text-foundation.test.js +++ b/test/unit/mdc-textfield/mdc-text-field-helper-text-foundation.test.js @@ -26,7 +26,7 @@ import td from 'testdouble'; import {verifyDefaultAdapter} from '../helpers/foundation'; import {setupFoundationTest} from '../helpers/setup'; -import MDCTextFieldHelperTextFoundation from '../../../packages/mdc-textfield/helper-text/foundation'; +import {MDCTextFieldHelperTextFoundation} from '../../../packages/mdc-textfield/helper-text/foundation'; const {cssClasses} = MDCTextFieldHelperTextFoundation; diff --git a/test/unit/mdc-textfield/mdc-text-field-icon-foundation.test.js b/test/unit/mdc-textfield/mdc-text-field-icon-foundation.test.js index 914860a2ce7..c7efc8b83fd 100644 --- a/test/unit/mdc-textfield/mdc-text-field-icon-foundation.test.js +++ b/test/unit/mdc-textfield/mdc-text-field-icon-foundation.test.js @@ -26,7 +26,7 @@ import td from 'testdouble'; import {verifyDefaultAdapter} from '../helpers/foundation'; import {setupFoundationTest} from '../helpers/setup'; -import MDCTextFieldIconFoundation from '../../../packages/mdc-textfield/icon/foundation'; +import {MDCTextFieldIconFoundation} from '../../../packages/mdc-textfield/icon/foundation'; import {strings} from '../../../packages/mdc-textfield/icon/constants'; suite('MDCTextFieldIconFoundation'); diff --git a/test/unit/mdc-toolbar/foundation.test.js b/test/unit/mdc-toolbar/foundation.test.js index b56ce6b383d..351852507e7 100644 --- a/test/unit/mdc-toolbar/foundation.test.js +++ b/test/unit/mdc-toolbar/foundation.test.js @@ -27,7 +27,7 @@ import td from 'testdouble'; import {install as installClock} from '../helpers/clock'; import {verifyDefaultAdapter} from '../helpers/foundation'; import {setupFoundationTest} from '../helpers/setup'; -import MDCToolbarFoundation from '../../../packages/mdc-toolbar/foundation'; +import {MDCToolbarFoundation} from '../../../packages/mdc-toolbar/foundation'; const {cssClasses, numbers} = MDCToolbarFoundation; diff --git a/test/unit/mdc-top-app-bar/fixed.foundation.test.js b/test/unit/mdc-top-app-bar/fixed.foundation.test.js index 3d885c0625a..b94a022bc1f 100644 --- a/test/unit/mdc-top-app-bar/fixed.foundation.test.js +++ b/test/unit/mdc-top-app-bar/fixed.foundation.test.js @@ -23,8 +23,8 @@ import td from 'testdouble'; -import MDCFixedTopAppBarFoundation from '../../../packages/mdc-top-app-bar/fixed/foundation'; -import MDCTopAppBarFoundation from '../../../packages/mdc-top-app-bar/foundation'; +import {MDCFixedTopAppBarFoundation} from '../../../packages/mdc-top-app-bar/fixed/foundation'; +import {MDCTopAppBarFoundation} from '../../../packages/mdc-top-app-bar/standard/foundation'; import {install as installClock} from '../helpers/clock'; suite('MDCFixedTopAppBarFoundation'); diff --git a/test/unit/mdc-top-app-bar/foundation.test.js b/test/unit/mdc-top-app-bar/foundation.test.js index 5b8094429fb..a9bb276889a 100644 --- a/test/unit/mdc-top-app-bar/foundation.test.js +++ b/test/unit/mdc-top-app-bar/foundation.test.js @@ -26,7 +26,7 @@ import td from 'testdouble'; import {captureHandlers} from '../helpers/foundation'; import {verifyDefaultAdapter} from '../helpers/foundation'; -import MDCTopAppBarBaseFoundation from '../../../packages/mdc-top-app-bar/foundation'; +import {MDCTopAppBarBaseFoundation} from '../../../packages/mdc-top-app-bar/foundation'; import {cssClasses, numbers, strings} from '../../../packages/mdc-top-app-bar/constants'; suite('MDCTopAppBarBaseFoundation'); diff --git a/test/unit/mdc-top-app-bar/mdc-top-app-bar.test.js b/test/unit/mdc-top-app-bar/mdc-top-app-bar.test.js index 90b231fa679..2afd33836c7 100644 --- a/test/unit/mdc-top-app-bar/mdc-top-app-bar.test.js +++ b/test/unit/mdc-top-app-bar/mdc-top-app-bar.test.js @@ -28,9 +28,9 @@ import td from 'testdouble'; import {MDCTopAppBar} from '../../../packages/mdc-top-app-bar/index'; import {strings} from '../../../packages/mdc-top-app-bar/constants'; -import MDCTopAppBarFoundation from '../../../packages/mdc-top-app-bar/foundation'; -import MDCFixedTopAppBarFoundation from '../../../packages/mdc-top-app-bar/fixed/foundation'; -import MDCShortTopAppBarFoundation from '../../../packages/mdc-top-app-bar/short/foundation'; +import {MDCTopAppBarFoundation} from '../../../packages/mdc-top-app-bar/standard/foundation'; +import {MDCFixedTopAppBarFoundation} from '../../../packages/mdc-top-app-bar/fixed/foundation'; +import {MDCShortTopAppBarFoundation} from '../../../packages/mdc-top-app-bar/short/foundation'; const MENU_ICONS_COUNT = 3; diff --git a/test/unit/mdc-top-app-bar/short.foundation.test.js b/test/unit/mdc-top-app-bar/short.foundation.test.js index 3c992681828..98b97135a0a 100644 --- a/test/unit/mdc-top-app-bar/short.foundation.test.js +++ b/test/unit/mdc-top-app-bar/short.foundation.test.js @@ -23,8 +23,8 @@ import td from 'testdouble'; -import MDCShortTopAppBarFoundation from '../../../packages/mdc-top-app-bar/short/foundation'; -import MDCTopAppBarFoundation from '../../../packages/mdc-top-app-bar/foundation'; +import {MDCShortTopAppBarFoundation} from '../../../packages/mdc-top-app-bar/short/foundation'; +import {MDCTopAppBarFoundation} from '../../../packages/mdc-top-app-bar/standard/foundation'; import {install as installClock} from '../helpers/clock'; suite('MDCShortTopAppBarFoundation'); diff --git a/test/unit/mdc-top-app-bar/standard.foundation.test.js b/test/unit/mdc-top-app-bar/standard.foundation.test.js index afb4465e0e0..be8ba4fde5b 100644 --- a/test/unit/mdc-top-app-bar/standard.foundation.test.js +++ b/test/unit/mdc-top-app-bar/standard.foundation.test.js @@ -24,7 +24,7 @@ import {assert} from 'chai'; import td from 'testdouble'; -import MDCTopAppBarFoundation from '../../../packages/mdc-top-app-bar/standard/foundation'; +import {MDCTopAppBarFoundation} from '../../../packages/mdc-top-app-bar/standard/foundation'; import {numbers} from '../../../packages/mdc-top-app-bar/constants'; import {install as installClock} from '../helpers/clock';