diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index afdf6bee1..1ea757b61 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -22,7 +22,7 @@ jobs: git config --local user.name "GitHub Action" yarn release $VERSION env: - VERSION: ${{github.event.inputs.version}} + VERSION: ${{ github.event.inputs.version }} - name: Build package run: yarn build - name: Publish @@ -30,7 +30,7 @@ jobs: npm set //registry.npmjs.org/:_authToken "$NPM_TOKEN" npm publish release env: - NPM_TOKEN: ${{secrets.NPM_TOKEN}} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Push changes uses: ad-m/github-push-action@master with: diff --git a/.gitignore b/.gitignore index 4baa0c07f..75b94e770 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .*cache *.log +*.tsbuildinfo +.idea coverage dist node_modules @@ -7,4 +9,3 @@ release reports /ng-package.debug.json /report.*.json -/.idea diff --git a/.storybook/IgnoreNotFoundExportPlugin.ts b/.storybook/IgnoreNotFoundExportPlugin.ts new file mode 100644 index 000000000..d24fd67c0 --- /dev/null +++ b/.storybook/IgnoreNotFoundExportPlugin.ts @@ -0,0 +1,52 @@ +/** + * https://github.com/TypeStrong/ts-loader/issues/653 + */ +import { Compiler, Plugin, Stats } from 'webpack'; + +const escapeStringForRegExp = (str: string) => + str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); + +declare class ModuleDependencyWarning { + message: string; +} + +export class IgnoreNotFoundExportPlugin implements Plugin { + constructor(private readonly exportsToIgnore: string[] = []) {} + + getMessageRegExp() { + if (this.exportsToIgnore.length > 0) { + const exportsPattern = + '(' + this.exportsToIgnore.map(escapeStringForRegExp).join('|') + ')'; + + return new RegExp( + `export '${exportsPattern}'( \\(reexported as '.*'\\))? was not found in`, + ); + } else { + return /export '.*'( \(reexported as '.*'\))? was not found in/; + } + } + + apply(compiler: Compiler) { + const messageRegExp = this.getMessageRegExp(); + + const doneHook = (stats: Stats) => { + stats.compilation.warnings = stats.compilation.warnings.filter( + (warn: ModuleDependencyWarning) => { + if ( + warn.constructor.name === 'ModuleDependencyWarning' && + messageRegExp.test(warn.message) + ) { + return false; + } + return true; + }, + ); + }; + + if (compiler.hooks) { + compiler.hooks.done.tap('IgnoreNotFoundExportPlugin', doneHook); + } else { + compiler.plugin('done', doneHook); + } + } +} diff --git a/.storybook/global.scss b/.storybook/global.scss index 297b9c1f3..304fa8936 100644 --- a/.storybook/global.scss +++ b/.storybook/global.scss @@ -1,4 +1,5 @@ -@import '../src/theme/style.scss'; +@import '../src/theme/style'; +@import '../src/theme/colors'; storybook-dynamic-app-root { &:first-child, @@ -11,3 +12,16 @@ storybook-dynamic-app-root { .sbdocs-table { width: 100%; } + +.d-block { + display: block; +} + +ol, +ul { + &.d-block li { + display: block; + width: unset; + height: unset; + } +} diff --git a/.storybook/main.ts b/.storybook/main.ts index 669173ba4..4c76a2811 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,23 +1,8 @@ import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'; import { Configuration } from 'webpack'; +import { merge } from 'webpack-merge'; -function webpackFinal(config: Configuration) { - config.resolve.plugins = [new TsconfigPathsPlugin()]; - - // story source addon - config.module.rules.push({ - test: /stories\/\S+\/index.ts$/, - loaders: [ - { - loader: require.resolve('@storybook/source-loader'), - options: { parser: 'typescript' }, - }, - ], - enforce: 'pre', - }); - - return config; -} +import { IgnoreNotFoundExportPlugin } from './IgnoreNotFoundExportPlugin'; export default { stories: [ @@ -29,6 +14,35 @@ export default { '@storybook/addon-knobs', '@storybook/addon-actions', '@storybook/addon-storysource', + 'storybook-dark-mode', ], - webpackFinal, + webpackFinal(config: Configuration) { + return merge(config, { + resolve: { + plugins: [ + new TsconfigPathsPlugin({ + configFile: '.storybook/tsconfig.json', + }), + ], + }, + module: { + rules: [ + { + test: /stories\/\S+\/index.ts$/, + loaders: [ + { + loader: require.resolve('@storybook/source-loader'), + options: { parser: 'typescript' }, + }, + ], + enforce: 'pre', + }, + ], + }, + plugins: [ + // remove useless import type warning + new IgnoreNotFoundExportPlugin(), + ], + }); + }, }; diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 5b93c6b90..d4bcd84f6 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1 +1,10 @@ -import '!style-loader!css-loader!sass-loader!./global.scss' +import '!style-loader!css-loader!sass-loader!./global.scss'; +import { addons } from '@storybook/addons'; + +const channel = addons.getChannel(); + +channel.addListener('DARK_MODE', (isDark: boolean) => { + document + .querySelector('html') + .setAttribute('aui-color-mode', isDark ? 'dark' : 'light'); +}); diff --git a/.storybook/tsconfig.json b/.storybook/tsconfig.json index 479fdcb86..b73defd06 100644 --- a/.storybook/tsconfig.json +++ b/.storybook/tsconfig.json @@ -1,5 +1,12 @@ { - "extends": "../tsconfig", - "include": ["../src/**/*", "../stories/**/*"], - "exclude": ["../src/**/*.spec.ts"] + "extends": "../tsconfig.base", + "include": ["../stories"], + // For vscode-ng-language-service + "angularCompilerOptions": { + "disableTypeScriptVersionCheck": true, + "fullTemplateTypeCheck": true, + "strictInjectionParameters": true, + "strictTemplates": true, + "trace": true + } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 922efc65c..25a9c54d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [5.6.3](https://github.com/alauda/alauda-ui/compare/v5.6.2...v5.6.3) (2021-03-15) + + +### Features + +* add anchor module ([#111](https://github.com/alauda/alauda-ui/issues/111)) ([47403b9](https://github.com/alauda/alauda-ui/commit/47403b9651d780c40f38e8af8d1a1154f8be8718)) +* support dark mode ([6e75972](https://github.com/alauda/alauda-ui/commit/6e759728c7e5872b233b3a4302ca98324912ba5f)) +* tag input and multi select support max row count ([#113](https://github.com/alauda/alauda-ui/issues/113)) ([ee1dcbf](https://github.com/alauda/alauda-ui/commit/ee1dcbfaec954fbea7c99686914ba83f26a14af3)) + ### [5.6.2](https://github.com/alauda/alauda-ui/compare/v5.6.1...v5.6.2) (2021-03-08) diff --git a/jest.config.js b/jest.config.js index 493b3bc84..a09a14495 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,6 @@ module.exports = { preset: 'jest-preset-angular', - setupFilesAfterEnv: ['/jest.setup.js'], + setupFilesAfterEnv: ['/jest.setup.ts'], testMatch: ['/src/**/+(*.)+(spec|test).+(ts|js)?(x)'], coverageReporters: ['text', 'html'], coverageDirectory: '/coverage', diff --git a/jest.setup.js b/jest.setup.ts similarity index 88% rename from jest.setup.js rename to jest.setup.ts index 19e80ece4..202e6bdb9 100644 --- a/jest.setup.js +++ b/jest.setup.ts @@ -1,23 +1,26 @@ -require('jest-preset-angular'); +import 'jest-preset-angular/setup-jest'; Object.defineProperty(window, 'CSS', { value: null }); + Object.defineProperty(document, 'doctype', { value: '', }); + Object.defineProperty(window, 'getComputedStyle', { - value: () => { + value() { return { display: 'none', appearance: ['-webkit-appearance'], }; }, }); + /** * ISSUE: https://github.com/angular/material2/issues/7101 * Workaround for JSDOM missing transform property */ Object.defineProperty(document.body.style, 'transform', { - value: () => { + value() { return { enumerable: true, configurable: true, diff --git a/package.json b/package.json index 40ec5b1b7..e9baef1d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@alauda/ui", - "version": "5.6.2", + "version": "5.6.3", "description": "Angular UI components by Alauda Frontend Team.", "repository": "git+https://github.com/alauda/alauda-ui.git", "author": "Alauda Frontend", @@ -21,10 +21,10 @@ "debug": "ng-packagr -p ng-package.debug.json && yarn build:copy --debug", "lint": "run-p lint:*", "lint:es": "cross-env PARSER_NO_WATCH=true eslint . --cache --ext js,md,mdx,ts -f friendly", - "lint:style": "stylelint --cache '{src,stories}/**/*.scss'", + "lint:style": "stylelint --cache '{src,stories}/**/*.{html,scss,ts}'", "lint:ts": "tslint -p . -t stylish", - "lint:tsc": "tsc --incremental false --noEmit", - "postinstall": "yarn-deduplicate || exit 0", + "lint:tsc": "tsc -p tsconfig.base.json --noEmit", + "postinstall": "yarn-deduplicate --strategy fewer || exit 0", "release": "standard-version --no-verify --release-as", "start": "yarn storybook", "storybook": "start-storybook -c .storybook -p 9001", @@ -46,52 +46,51 @@ "rxjs": "^6.0.0" }, "devDependencies": { - "@1stg/app-config": "^1.1.2", - "@1stg/lib-config": "^1.0.4", - "@1stg/tslint-config": "^1.0.0", - "@angular-devkit/build-angular": "^0.1102.1", - "@angular-devkit/core": "^11.2.1", - "@angular/animations": "^11.2.1", - "@angular/cdk": "^11.2.1", - "@angular/cli": "^11.2.1", - "@angular/common": "^11.2.1", - "@angular/compiler": "^11.2.1", - "@angular/compiler-cli": "^11.2.1", - "@angular/core": "^11.2.1", - "@angular/forms": "^11.2.1", - "@angular/platform-browser": "^11.2.1", - "@angular/platform-browser-dynamic": "^11.2.1", - "@storybook/addon-actions": "^6.1.18", - "@storybook/addon-docs": "^6.1.18", - "@storybook/addon-knobs": "^6.1.18", - "@storybook/addon-storysource": "^6.1.18", - "@storybook/addons": "^6.1.18", - "@storybook/angular": "^6.1.18", - "@storybook/cli": "^6.1.18", - "@storybook/source-loader": "^6.1.18", + "@1stg/app-config": "^1.2.9", + "@1stg/tslint-config": "^1.1.0", + "@angular-devkit/build-angular": "^0.1102.3", + "@angular-devkit/core": "^11.2.3", + "@angular/animations": "^11.2.4", + "@angular/cdk": "^11.2.3", + "@angular/cli": "^11.2.3", + "@angular/common": "^11.2.4", + "@angular/compiler": "^11.2.4", + "@angular/compiler-cli": "^11.2.4", + "@angular/core": "^11.2.4", + "@angular/forms": "^11.2.4", + "@angular/platform-browser": "^11.2.4", + "@angular/platform-browser-dynamic": "^11.2.4", + "@storybook/addon-actions": "^6.1.21", + "@storybook/addon-docs": "^6.1.21", + "@storybook/addon-knobs": "^6.1.21", + "@storybook/addon-storysource": "^6.1.21", + "@storybook/addons": "^6.1.21", + "@storybook/angular": "^6.1.21", + "@storybook/cli": "^6.1.21", + "@storybook/source-loader": "^6.1.21", "@types/jest": "^26.0.20", - "@types/node": "^14.14.30", + "@types/node": "^14.14.31", + "@types/resize-observer-browser": "^0.1.5", "clipboard-polyfill": "^3.0.2", "gulp": "^4.0.2", "gulp-dart-sass": "^1.0.2", "jest": "^26.6.3", "jest-junit": "^12.0.0", - "jest-preset-angular": "^8.3.2", - "ng-packagr": "^11.2.2", - "ng-tslint": "^1.1.0", + "jest-preset-angular": "^8.4.0", + "ng-packagr": "^11.2.4", "npm-run-all": "^4.1.5", - "rxjs": "^6.6.2", + "postcss": "^8.2.7", + "rxjs": "^6.6.6", "standard-version": "^9.1.1", + "storybook-dark-mode": "^1.0.4", "ts-node": "^9.1.1", "tsconfig-paths-webpack-plugin": "^3.3.0", "tslib": "^2.1.0", "tslint": "^6.1.3", - "typescript": "^4.1.5", + "webpack-merge": "^5.7.3", "yarn-deduplicate": "^3.1.0" }, "resolutions": { - "prettier": "^2.2.1", - "sass-loader": "^10.1.1", - "webpack": "^4.46.0" + "prettier": "^2.2.1" } } diff --git a/src/anchor/README.md b/src/anchor/README.md new file mode 100644 index 000000000..0cdf76da5 --- /dev/null +++ b/src/anchor/README.md @@ -0,0 +1,23 @@ +# Anchor Module + +## AnchorDirective(`[auiAnchor]`) + +### AnchorDirective Inputs + +| 参数 | 说明 | 类型 | 默认值 | +| ------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------- | ----------- | +| auiAnchor | 滚动容器, 不传值默认取 `cdkScrollable` 或当前元素(如果可滚动)或 `window` | `HTMLElement` | `undefined` | +| adaptPosition | 是否自动调整当前元素 `paddingRight` 以保证与自动生成的锚点内容重叠 | `boolean` | `true` | +| padding | 自动调整 `paddingRight` 时自动保持与自动生成的锚点内容的距离 | `number` | `20` | +| minTop | 当滚动页面时自动生成的锚点内容距离页面顶部最小距离,默认取当前 `.aui-page__content` 元素的 `paddingTop` 值 | `number` | `20` | +| injectId | 是否自动在 `auiAnchorLabel` 元素上注入 id,将自动生成 `#` 可记录锚点 | `window === window.top` | + +## AnchorLabelDirective(`[auiAnchorLabel]`) + +### AnchorLabelDirective Inputs + +| 参数 | 说明 | 类型 | 默认值 | +| --------------------- | ------------------------------------------------------------- | ----------------------- | ----------- | +| auiAnchorLabel | 自动生成锚点时对应文本内容,默认取当前元素文本内容 | `string \| TemplateRef` | `undefined` | +| auiAnchorLabelContext | 使用 `TemplateRef` 类型 `auiAnchorLabel` 时的上下文 | `unknown` | `undefined` | +| level | 锚点层级,不写这个 `input` 默认是 `0`,写了不填内容默认是 `1` | `string \| TemplateRef` | `0` | diff --git a/src/anchor/anchor-tree.component.html b/src/anchor/anchor-tree.component.html new file mode 100644 index 000000000..d4c982195 --- /dev/null +++ b/src/anchor/anchor-tree.component.html @@ -0,0 +1,25 @@ +
+ + + + + + {{ item.label }} + + + +
diff --git a/src/anchor/anchor.component.html b/src/anchor/anchor.component.html new file mode 100644 index 000000000..086b42954 --- /dev/null +++ b/src/anchor/anchor.component.html @@ -0,0 +1,6 @@ +
+ +
diff --git a/src/anchor/anchor.component.scss b/src/anchor/anchor.component.scss new file mode 100644 index 000000000..17036ec5e --- /dev/null +++ b/src/anchor/anchor.component.scss @@ -0,0 +1,116 @@ +@import '../theme/mixin'; +@import '../theme/var'; + +aui-anchor { + display: block; + position: fixed; + z-index: 1; + white-space: nowrap; +} + +.aui-anchor { + padding: 30px 16px; + border-left: 1px solid $card-border-color; + + &:before { + content: ''; + position: absolute; + left: 0; + top: 0; + transform: translate3d(-50%, -50%, 0); + @include point(5px, $card-border-color); + } + + &:after { + content: ''; + position: absolute; + left: 0; + bottom: 0; + transform: translate3d(-50%, 50%, 0); + @include point(5px, $card-border-color); + } + + $p: &; + + &__label { + scroll-margin-top: $page-content-padding; + } + + @at-root .aui-page { + &.hasHeader #{$p}__label { + scroll-margin-top: $page-header-height + $page-content-padding; + } + + &.hasToolbar #{$p}__label { + scroll-margin-top: $page-toolbar-height + $page-content-padding; + } + + &.hasHeader.hasToolbar #{$p}__label { + scroll-margin-top: $page-header-height + $page-toolbar-height + + $page-content-padding; + } + } + + &__item { + position: relative; + margin-bottom: 12px; + + &:before { + content: ''; + position: absolute; + top: 9px; + left: -16px; + transform: translate3d(-50%, -50%, 0); + box-sizing: border-box; + border: 1px solid $color-text-help; + @include point(9px, #fff); + } + + a { + color: $color-text-main; + + &:hover { + color: $color-primary; + } + } + + $p: &; + + &.active { + &:before { + border: 0; + @include point(9px, $color-primary); + } + + &, + & & { + a { + color: $color-primary; + } + } + + #{$p}:not(.active) a { + color: $color-text-secondary; + } + } + + & & { + margin-top: 4px; + margin-bottom: 0; + margin-left: 14px; + font-size: 10px; + + &:before { + display: none; + } + + a { + color: $color-text-help; + + &:hover { + color: $color-primary; + } + } + } + } +} diff --git a/src/anchor/anchor.component.ts b/src/anchor/anchor.component.ts new file mode 100644 index 000000000..cc24e70bc --- /dev/null +++ b/src/anchor/anchor.component.ts @@ -0,0 +1,207 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Injector, + Input, + OnDestroy, + Output, + ViewEncapsulation, +} from '@angular/core'; +import { EMPTY, Subject, combineLatest, fromEvent, of } from 'rxjs'; +import { debounceTime, switchMap, takeUntil, tap } from 'rxjs/operators'; + +import { buildBem, isTemplateRef, last } from '../utils'; + +import { AnchorDirectiveChild } from './anchor.directive'; +import { AnchorItem, AnchorTreeItem } from './types'; +import { getAnchorTreeItems } from './utils'; + +const bem = buildBem('aui-anchor'); + +@Component({ + selector: 'aui-anchor-tree', + templateUrl: 'anchor-tree.component.html', + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AnchorTreeComponent + extends AnchorDirectiveChild + implements OnDestroy { + @Input() + set treeItems(treeItems) { + this._treeItems = treeItems; + this.watchLabelsChange(); + } + + get treeItems() { + return this._treeItems; + } + + private _treeItems: AnchorTreeItem[]; + + @Input() + activeId: string; + + @Output() + activeIdChange = new EventEmitter(); + + bem = bem; + + isTemplateRef = isTemplateRef; + + private readonly depose$$ = new Subject(); + + watchLabelsChange() { + this.depose$$.next(); + const cdr = this.injector.get(ChangeDetectorRef); + // FIXME: Is there any better way to achieve this? + combineLatest( + this.treeItems.map(({ labelChange }) => labelChange).filter(Boolean), + ) + .pipe(debounceTime(0), takeUntil(this.depose$$)) + .subscribe(() => cdr.markForCheck()); + } + + ngOnDestroy() { + this.depose$$.next(); + this.depose$$.complete(); + } + + isActive(item: AnchorTreeItem): boolean { + return ( + item.id === this.activeId || + item.children?.some(subItem => this.isActive(subItem)) + ); + } + + onItemClick(e: Event, item: AnchorTreeItem) { + e.preventDefault(); + + if (this.parent.injectId) { + const selector = '#' + item.id; + + if (location.hash === selector) { + return; + } + + history.pushState( + null, + null, + location.pathname + location.search + selector, + ); + } + + item.target.scrollIntoView({ behavior: 'smooth' }); + + this.activeIdChange.emit(item.id); + } + + trackById(_index: number, item: AnchorItem) { + return item.id; + } +} + +@Component({ + selector: 'aui-anchor', + templateUrl: 'anchor.component.html', + styleUrls: ['anchor.component.scss'], + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AnchorComponent + extends AnchorDirectiveChild + implements AfterViewInit, OnDestroy { + @Input() + get items() { + return this._items; + } + + set items(items) { + this._items = items; + this.treeItems = getAnchorTreeItems(items); + const anchorId = + (this.parent.injectId && location.hash.slice(1)) || this.activeId; + const activeItem = + (anchorId && items.find(({ id }) => id === anchorId)) || items[0]; + if (activeItem) { + if (this.parent.injectId && this.parent.scrollableEl !== window) { + activeItem.target.scrollIntoView(); + } + this.activeId = activeItem.id; + } + } + + private _items: AnchorItem[]; + + @Input() + treeItems: AnchorTreeItem[]; + + @Input() + activeId: string; + + bem = bem; + + private readonly destroy$$ = new Subject(); + + constructor( + protected injector: Injector, + private readonly cdr: ChangeDetectorRef, + ) { + super(injector); + } + + ngAfterViewInit() { + const { injectId, containerEl, scrollableEl } = this.parent; + const pageContentEl = containerEl.closest('.aui-page__content'); + const paddingTop = pageContentEl + ? +getComputedStyle(pageContentEl).paddingTop.slice(0, -2) + : 0; + fromEvent(scrollableEl, 'scroll') + .pipe( + debounceTime(100), + switchMap(() => { + const { scrollTop } = + scrollableEl === window + ? document.documentElement + : (scrollableEl as HTMLElement); + const activedItem = + this.items.find( + ({ target }) => + target.offsetTop + + target.offsetHeight / 2 + + ((scrollableEl === window && + (target.offsetParent as HTMLElement)?.offsetTop) || + 0) > + scrollTop + paddingTop, + ) || last(this.items); + return activedItem ? of(activedItem) : EMPTY; + }), + tap(activedItem => { + if (activedItem.id) { + this.activeId = activedItem.id; + this.cdr.markForCheck(); + } + }), + debounceTime(100), + tap(activedItem => { + if (injectId && activedItem.id) { + history.replaceState( + null, + null, + location.pathname + location.search + '#' + activedItem.id, + ); + } + }), + takeUntil(this.destroy$$), + ) + .subscribe(); + } + + ngOnDestroy() { + this.destroy$$.next(); + this.destroy$$.complete(); + } +} diff --git a/src/anchor/anchor.directive.ts b/src/anchor/anchor.directive.ts new file mode 100644 index 000000000..7d45b90a3 --- /dev/null +++ b/src/anchor/anchor.directive.ts @@ -0,0 +1,229 @@ +import { ComponentPortal, DomPortalOutlet } from '@angular/cdk/portal'; +import { CdkScrollable } from '@angular/cdk/scrolling'; +import { + AfterContentChecked, + AfterContentInit, + ApplicationRef, + ComponentFactoryResolver, + ContentChildren, + Directive, + ElementRef, + EventEmitter, + HostBinding, + Injector, + Input, + OnDestroy, + OnInit, + Optional, + Output, + QueryList, + TemplateRef, +} from '@angular/core'; +import { Subject, fromEvent, merge } from 'rxjs'; +import { startWith, takeUntil } from 'rxjs/operators'; + +import { observeResizeOn } from '../utils'; + +import { AnchorComponent } from './anchor.component'; + +let uid = 0; + +@Directive() +// tslint:disable-next-line: directive-class-suffix +export class AnchorDirectiveChild { + readonly parent: AnchorDirective; + + constructor(protected injector: Injector) { + this.parent = this.injector.get(AnchorDirective); + } +} + +@Directive({ + selector: '[auiAnchorLabel]', +}) +export class AnchorLabelDirective + extends AnchorDirectiveChild + implements OnInit, AfterContentChecked { + @HostBinding('class.aui-anchor__label') + @Input('auiAnchorLabel') + get label() { + return this._label || this.elRef.nativeElement.textContent; + } + + set label(value) { + this._label = value; + this.labelChange.emit(value); + } + + private _label: string | TemplateRef; + + @Input('auiAnchorLabelContext') + labelContext: unknown; + + get target() { + return this.elRef.nativeElement; + } + + @Input() + set id(id) { + if (this.parent.injectId && !this.id) { + this.elRef.nativeElement.id = id; + } + this._id = id; + } + + get id() { + return this._id; + } + + private _id: string = this.elRef.nativeElement.id; + + @Input() + set level(value: number | '') { + this._level = value === '' ? 1 : value; + } + + get level() { + return this._level; + } + + private _level = 0; + + @Output() + labelChange = new EventEmitter>(); + + constructor( + protected injector: Injector, + public readonly elRef: ElementRef, + ) { + super(injector); + } + + ngOnInit() { + if (!this.id) { + this.id = 'anchor-uid-' + uid++; + } + } + + ngAfterContentChecked() { + if (!this._label) { + this.labelChange.emit(this.label); + } + } +} + +@Directive({ + selector: '[auiAnchor]', +}) +export class AnchorDirective implements AfterContentInit, OnDestroy { + @Input() + auiAnchor: HTMLElement | ''; + + @Input() + adaptPosition = true; + + @Input() + padding = 20; + + @Input() + minTop: number; + + @Input() + injectId = window === window.top; + + @ContentChildren(AnchorLabelDirective, { descendants: true }) + anchorLabels: QueryList; + + get containerEl() { + return this.elRef.nativeElement; + } + + get scrollableEl() { + const el = this.containerEl; + return ( + this.auiAnchor || + this.cdkScrollable?.getElementRef().nativeElement || + (el.scrollHeight > el.offsetHeight ? el : window) + ); + } + + anchorPortal: ComponentPortal; + + destroy$$ = new Subject(); + + constructor( + private readonly cfr: ComponentFactoryResolver, + private readonly appRef: ApplicationRef, + private readonly injector: Injector, + public readonly elRef: ElementRef, + @Optional() private readonly cdkScrollable: CdkScrollable, + ) {} + + ngAfterContentInit() { + const containerEl = this.containerEl; + this.anchorPortal = new ComponentPortal(AnchorComponent); + const portalOutlet = new DomPortalOutlet( + document.body, + this.cfr, + this.appRef, + this.injector, + ); + const anchorComponentRef = this.anchorPortal.attach(portalOutlet); + const anchorEl = anchorComponentRef.injector.get(ElementRef) + .nativeElement as HTMLElement; + + requestAnimationFrame(() => + this.adaptAnchorPosition(containerEl, anchorEl), + ); + + this.anchorLabels.changes + .pipe(startWith(this.anchorLabels), takeUntil(this.destroy$$)) + .subscribe((anchorLabels: QueryList) => { + Object.assign(anchorComponentRef.instance, { + items: anchorLabels.toArray(), + }); + }); + } + + ngOnDestroy() { + this.destroy$$.next(); + this.destroy$$.complete(); + this.anchorPortal.detach(); + } + + adaptAnchorPosition(containerEl: HTMLElement, anchorEl: HTMLElement) { + const pageContentEl = containerEl.closest('.aui-page__content'); + const anchorContentEl = anchorEl.querySelector('.aui-anchor'); + + merge(observeResizeOn(containerEl), fromEvent(window, 'scroll')) + .pipe(startWith(null as void), takeUntil(this.destroy$$)) + .subscribe(() => { + const containerRect = containerEl.getBoundingClientRect(); + Object.assign(anchorEl.style, { + display: !containerRect.width || !containerRect.height ? 'none' : '', + left: + containerRect.right - + anchorContentEl.getBoundingClientRect().width + + 'px', + top: + Math.max( + containerRect.top, + (this.minTop ?? + (pageContentEl && + +getComputedStyle(pageContentEl).paddingTop.slice(0, -2))) || + 0, + ) + 'px', + }); + }); + + if (this.adaptPosition) { + observeResizeOn(anchorContentEl) + .pipe(takeUntil(this.destroy$$)) + .subscribe(el => { + const width = el.getBoundingClientRect().width; + const padding = width + this.padding; + containerEl.style.paddingRight = padding + 'px'; + }); + } + } +} diff --git a/src/anchor/anchor.module.ts b/src/anchor/anchor.module.ts new file mode 100644 index 000000000..02d847b0b --- /dev/null +++ b/src/anchor/anchor.module.ts @@ -0,0 +1,23 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { AnchorComponent, AnchorTreeComponent } from './anchor.component'; +import { AnchorDirective, AnchorLabelDirective } from './anchor.directive'; + +@NgModule({ + imports: [CommonModule], + declarations: [ + AnchorComponent, + AnchorTreeComponent, + AnchorDirective, + AnchorLabelDirective, + ], + exports: [ + AnchorComponent, + AnchorTreeComponent, + AnchorDirective, + AnchorLabelDirective, + ], + entryComponents: [AnchorComponent], +}) +export class AnchorModule {} diff --git a/src/anchor/public-api.ts b/src/anchor/public-api.ts new file mode 100644 index 000000000..7ac8cccf7 --- /dev/null +++ b/src/anchor/public-api.ts @@ -0,0 +1,5 @@ +export * from './anchor.component'; +export * from './anchor.directive'; +export * from './anchor.module'; +export * from './types'; +export * from './utils'; diff --git a/src/anchor/types.ts b/src/anchor/types.ts new file mode 100644 index 000000000..bca4f8053 --- /dev/null +++ b/src/anchor/types.ts @@ -0,0 +1,17 @@ +import { EventEmitter, TemplateRef } from '@angular/core'; + +export interface AnchorItemBase { + readonly id?: string; + readonly target?: HTMLElement; + readonly label: string | TemplateRef; + readonly labelContext?: unknown; + readonly labelChange?: EventEmitter>; +} + +export interface AnchorItem extends AnchorItemBase { + readonly level: number; +} + +export interface AnchorTreeItem extends AnchorItemBase { + children?: AnchorTreeItem[]; +} diff --git a/src/anchor/utils.spec.ts b/src/anchor/utils.spec.ts new file mode 100644 index 000000000..1ab6d5627 --- /dev/null +++ b/src/anchor/utils.spec.ts @@ -0,0 +1,101 @@ +import { getAnchorTreeItems } from './utils'; + +describe('getAnchorTreeItems', () => { + it('should work as expected', () => { + expect( + getAnchorTreeItems([ + { + id: '1', + label: '1', + level: 0, + }, + { + id: '1-1', + label: '1-1', + level: 1, + }, + { + id: '1-2', + label: '1-2', + level: 1, + }, + { + id: '2', + label: '2', + level: 0, + }, + { + id: '3', + label: '3', + level: 0, + }, + { + id: '3-1', + label: '3-1', + level: 1, + }, + { + id: '3-1-1', + label: '3-1-1', + level: 2, + }, + { + id: '3-1-2', + label: '3-1-2', + level: 2, + }, + { + id: '3-2', + label: '3-2', + level: 1, + }, + { + id: '4', + label: '4', + level: 0, + }, + ]), + ).toEqual([ + { + id: '1', + label: '1', + level: 0, + children: [ + { id: '1-1', label: '1-1', level: 1 }, + { id: '1-2', label: '1-2', level: 1 }, + ], + }, + { + id: '2', + label: '2', + level: 0, + }, + { + id: '3', + label: '3', + level: 0, + children: [ + { + id: '3-1', + label: '3-1', + level: 1, + children: [ + { id: '3-1-1', label: '3-1-1', level: 2 }, + { id: '3-1-2', label: '3-1-2', level: 2 }, + ], + }, + { + id: '3-2', + label: '3-2', + level: 1, + }, + ], + }, + { + id: '4', + label: '4', + level: 0, + }, + ]); + }); +}); diff --git a/src/anchor/utils.ts b/src/anchor/utils.ts new file mode 100644 index 000000000..d5cb983ca --- /dev/null +++ b/src/anchor/utils.ts @@ -0,0 +1,27 @@ +import { last } from '../utils'; + +import { AnchorItem, AnchorTreeItem } from './types'; + +export const getAnchorTreeItems = (items: AnchorItem[], level = 0) => { + const treeItems: AnchorTreeItem[] = []; + const subItems: AnchorItem[] = []; + for (const item of items) { + if (item.level === level) { + const lastTreeItem = last(treeItems); + if (lastTreeItem) { + const children = getAnchorTreeItems(subItems, level + 1); + if (children.length) { + lastTreeItem.children = children; + } + } + treeItems.push(item); + subItems.length = 0; + } else { + subItems.push(item); + } + } + if (subItems.length) { + last(treeItems).children = getAnchorTreeItems(subItems, level + 1); + } + return treeItems; +}; diff --git a/src/autocomplete/autocomplete.directive.ts b/src/autocomplete/autocomplete.directive.ts index 151cb6d74..dc666d3a0 100644 --- a/src/autocomplete/autocomplete.directive.ts +++ b/src/autocomplete/autocomplete.directive.ts @@ -25,7 +25,7 @@ import { TooltipTrigger, TooltipType, } from '../tooltip/public-api'; -import { scrollIntoView } from '../utils/scroll-into-view'; +import { scrollIntoView } from '../utils'; import { AutocompleteComponent } from './autocomplete.component'; import { AutoCompleteContext, SuggestionFilterFn } from './autocomplete.types'; diff --git a/src/autocomplete/helper-directives.ts b/src/autocomplete/helper-directives.ts index 51e8c98e6..c8c8d92c1 100644 --- a/src/autocomplete/helper-directives.ts +++ b/src/autocomplete/helper-directives.ts @@ -7,5 +7,4 @@ import { Directive } from '@angular/core'; '[class.aui-suggestion-group__title]': 'true', }, }) -// eslint-disable-next-line @typescript-eslint/no-extraneous-class export class SuggestionGroupTitleDirective {} diff --git a/src/autocomplete/suggestion/suggestion.component.ts b/src/autocomplete/suggestion/suggestion.component.ts index 06a9c7613..6be8ec1c6 100644 --- a/src/autocomplete/suggestion/suggestion.component.ts +++ b/src/autocomplete/suggestion/suggestion.component.ts @@ -19,8 +19,7 @@ import { tap, } from 'rxjs/operators'; -import { Bem, buildBem } from '../../utils/bem'; -import { coerceAttrBoolean } from '../../utils/coercion'; +import { Bem, buildBem, coerceAttrBoolean } from '../../utils'; import { AutocompleteComponent } from '../autocomplete.component'; @Component({ diff --git a/src/back-top/back-top.component.ts b/src/back-top/back-top.component.ts index 61b3c2c2b..a3ab7bbe7 100644 --- a/src/back-top/back-top.component.ts +++ b/src/back-top/back-top.component.ts @@ -68,17 +68,13 @@ export class BackTopComponent { fromEvent(target, 'scroll').pipe( // use default scheduler throttleTime(50, undefined, { leading: true, trailing: true }), - map(() => { - return this.getTargetScrollTop(target); - }), + map(() => this.getTargetScrollTop(target)), ), ), ), this.visibilityHeight$$, ]).pipe( - map(([scrollTop, visibilityHeight]) => { - return scrollTop >= visibilityHeight; - }), + map(([scrollTop, visibilityHeight]) => scrollTop >= visibilityHeight), distinctUntilChanged(), ); diff --git a/src/button/button.component.scss b/src/button/button.component.scss index 00191ce3d..42794e398 100644 --- a/src/button/button.component.scss +++ b/src/button/button.component.scss @@ -208,11 +208,12 @@ $button-size: ( color: $button-primary-border; background-color: $color-white; border-color: $button-primary-border; - $hover-bg: mix($color-white, $button-primary-border, 90%); - &:hover { + &:hover, + &:active, + &.isActive { color: $button-primary-border; - background-color: $hover-bg; + background-color: $color-primary-ultra-light; border-color: $button-primary-border; } @@ -223,13 +224,6 @@ $button-size: ( @include outline-shadow($outline-color-primary); } - &:active, - &.isActive { - color: mix($color-black, $button-primary-border, 20%); - background-color: $hover-bg; - border-color: $button-primary-border; - } - &[disabled] { color: $button-default-content-disabled; background-color: $button-default-bg-disabled; @@ -273,11 +267,12 @@ $button-size: ( color: $button-success-border; background-color: $color-white; border-color: $button-success-border; - $hover-bg: mix($color-white, $button-success-border, 90%); - &:hover { + &:hover, + &:active, + &.isActive { color: $button-success-border; - background-color: $hover-bg; + background-color: $color-success-ultra-light; border-color: $button-success-border; } @@ -288,13 +283,6 @@ $button-size: ( @include outline-shadow($outline-color-success); } - &:active, - &.isActive { - color: mix($color-black, $button-success-border, 20%); - background-color: $hover-bg; - border-color: $button-success-border; - } - &[disabled] { color: $button-default-content-disabled; background-color: $button-default-bg-disabled; @@ -338,11 +326,12 @@ $button-size: ( color: $button-warning-border; background-color: $color-white; border-color: $button-warning-border; - $hover-bg: mix($color-white, $button-warning-border, 90%); - &:hover { + &:hover, + &:active, + &.isActive { color: $button-warning-border; - background-color: $hover-bg; + background-color: $color-warning-ultra-light; border-color: $button-warning-border; } @@ -353,13 +342,6 @@ $button-size: ( @include outline-shadow($outline-color-warning); } - &:active, - &.isActive { - color: mix($color-black, $button-warning-border, 20%); - background-color: $hover-bg; - border-color: $button-warning-border; - } - &[disabled] { color: $button-default-content-disabled; background-color: $button-default-bg-disabled; @@ -405,11 +387,12 @@ $button-size: ( color: $button-danger-border; background-color: $color-white; border-color: $button-danger-border; - $hover-bg: mix($color-white, $button-danger-border, 90%); - &:hover { + &:hover, + &:active, + &.isActive { color: $button-danger-border; - background-color: $hover-bg; + background-color: $color-danger-ultra-light; border-color: $button-danger-border; } @@ -420,13 +403,6 @@ $button-size: ( @include outline-shadow($outline-color-error); } - &:active, - &.isActive { - color: mix($color-black, $button-danger-border, 20%); - background-color: $hover-bg; - border-color: $button-danger-border; - } - &[disabled] { color: $button-default-content-disabled; background-color: $button-default-bg-disabled; @@ -470,11 +446,12 @@ $button-size: ( color: $button-info-border; background-color: $color-white; border-color: $button-info-border; - $hover-bg: mix($color-white, $button-info-border, 90%); - &:hover { + &:hover, + &:active, + &.isActive { color: $button-info-border; - background-color: $hover-bg; + background-color: $color-info-ultra-light; border-color: $button-info-border; } @@ -485,13 +462,6 @@ $button-size: ( @include outline-shadow($outline-color-info); } - &:active, - &.isActive { - color: mix($color-black, $button-info-border, 20%); - background-color: $hover-bg; - border-color: $button-info-border; - } - &[disabled] { color: $button-default-content-disabled; background-color: $button-default-bg-disabled; diff --git a/src/card/helper-directives.ts b/src/card/helper-directives.ts index 8e39fbff5..e06b38778 100644 --- a/src/card/helper-directives.ts +++ b/src/card/helper-directives.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-extraneous-class */ // tslint:disable: no-host-metadata-property import { Directive } from '@angular/core'; diff --git a/src/checkbox/checkbox.component.ts b/src/checkbox/checkbox.component.ts index 71efa32a1..302599bda 100644 --- a/src/checkbox/checkbox.component.ts +++ b/src/checkbox/checkbox.component.ts @@ -91,12 +91,11 @@ export class CheckboxComponent takeUntil(this.destroy$$), map(([value, label]) => { if (this.checkboxGroup.trackFn) { - return value?.some(v => { - return ( + return value?.some( + v => this.checkboxGroup.trackFn(v) === - this.checkboxGroup.trackFn(label) - ); - }); + this.checkboxGroup.trackFn(label), + ); } return value?.includes(label); }), diff --git a/src/dialog/confirm-dialog/confirm-dialog.component.ts b/src/dialog/confirm-dialog/confirm-dialog.component.ts index e8aa0b0a3..66003ba70 100644 --- a/src/dialog/confirm-dialog/confirm-dialog.component.ts +++ b/src/dialog/confirm-dialog/confirm-dialog.component.ts @@ -6,7 +6,7 @@ import { } from '@angular/core'; import { Observable } from 'rxjs'; -import { Bem, buildBem } from '../../utils/bem'; +import { Bem, buildBem } from '../../utils'; import { DialogRef } from '../dialog-ref'; import { BeforeAction, diff --git a/src/dialog/dialog-content/dialog-content.component.ts b/src/dialog/dialog-content/dialog-content.component.ts index 5d97c9d04..e726b3c6c 100644 --- a/src/dialog/dialog-content/dialog-content.component.ts +++ b/src/dialog/dialog-content/dialog-content.component.ts @@ -8,7 +8,7 @@ import { ViewEncapsulation, } from '@angular/core'; -import { Bem, buildBem } from '../../utils/bem'; +import { Bem, buildBem } from '../../utils'; @Component({ selector: 'aui-dialog-content', diff --git a/src/dialog/dialog-content/dialog-footer.component.ts b/src/dialog/dialog-content/dialog-footer.component.ts index d0557cc07..500158899 100644 --- a/src/dialog/dialog-content/dialog-footer.component.ts +++ b/src/dialog/dialog-content/dialog-footer.component.ts @@ -4,7 +4,7 @@ import { ViewEncapsulation, } from '@angular/core'; -import { Bem, buildBem } from '../../utils/bem'; +import { Bem, buildBem } from '../../utils'; @Component({ selector: 'aui-dialog-footer', diff --git a/src/dialog/dialog-content/dialog-header.component.ts b/src/dialog/dialog-content/dialog-header.component.ts index 82340e4cb..09f38b3ef 100644 --- a/src/dialog/dialog-content/dialog-header.component.ts +++ b/src/dialog/dialog-content/dialog-header.component.ts @@ -8,7 +8,7 @@ import { ViewEncapsulation, } from '@angular/core'; -import { Bem, buildBem } from '../../utils/bem'; +import { Bem, buildBem } from '../../utils'; import { DialogRef } from '../dialog-ref'; import { DialogService } from '../dialog.service'; import { getClosestDialog } from '../utils'; diff --git a/src/dialog/dialog.component.ts b/src/dialog/dialog.component.ts index eef56e5df..e9b4fac3c 100644 --- a/src/dialog/dialog.component.ts +++ b/src/dialog/dialog.component.ts @@ -13,10 +13,10 @@ import { ViewEncapsulation, } from '@angular/core'; -import { Bem, buildBem } from '../utils/bem'; +import { Bem, buildBem } from '../utils'; import { DialogConfig } from './dialog-config'; -import { throwDialogContentAlreadyAttachedError } from './utils/errors'; +import { throwDialogContentAlreadyAttachedError } from './utils'; @Component({ selector: 'aui-dialog', diff --git a/src/dialog/dialog.service.spec.ts b/src/dialog/dialog.service.spec.ts index 0f433bac2..b9a2ee4e4 100644 --- a/src/dialog/dialog.service.spec.ts +++ b/src/dialog/dialog.service.spec.ts @@ -69,8 +69,8 @@ describe('DialogService', () => { ); }); - it('should be closed by click close button', () => { - return new Promise(resolve => { + it('should be closed by click close button', () => + new Promise(resolve => { const ins = fixture.componentInstance; ins.result = 'result'; dialogService @@ -84,11 +84,10 @@ describe('DialogService', () => { fixture.detectChanges(); ocEl.querySelector('#close').dispatchEvent(new Event('click')); fixture.detectChanges(); - }); - }); + })); - it('should open confirm dialog and invoke confirm callback', () => { - return new Promise(resolve => { + it('should open confirm dialog and invoke confirm callback', () => + new Promise(resolve => { dialogService .confirm({ title: 'title', @@ -103,11 +102,10 @@ describe('DialogService', () => { ocEl .querySelector('.aui-confirm-dialog__confirm-button') .dispatchEvent(new Event('click')); - }); - }); + })); - it('should open confirm dialog and invoke cancel callback', () => { - return new Promise(resolve => { + it('should open confirm dialog and invoke cancel callback', () => + new Promise(resolve => { dialogService .confirm({ title: 'custom title', @@ -128,11 +126,10 @@ describe('DialogService', () => { ocEl .querySelector('.aui-confirm-dialog__cancel-button') .dispatchEvent(new Event('click')); - }); - }); + })); - it('should before confirm work correctly', () => { - return new Promise(resolve => { + it('should before confirm work correctly', () => + new Promise(resolve => { const t1 = Date.now(); dialogService .confirm({ @@ -156,11 +153,10 @@ describe('DialogService', () => { expect(confirmBtn.className).toContain('isLoading'); expect(confirmBtn.disabled).toBeTruthy(); - }); - }); + })); - it('should before confirm observable work correctly', () => { - return new Promise(resolve => { + it('should before confirm observable work correctly', () => + new Promise(resolve => { const t1 = Date.now(); dialogService .confirm({ @@ -183,11 +179,10 @@ describe('DialogService', () => { expect(confirmBtn.className).toContain('isLoading'); expect(confirmBtn.disabled).toBeTruthy(); - }); - }); + })); - it('should before cancel work correctly', () => { - return new Promise(resolve => { + it('should before cancel work correctly', () => + new Promise(resolve => { const t1 = Date.now(); dialogService .confirm({ @@ -212,11 +207,10 @@ describe('DialogService', () => { expect(cancelBtn.className).toContain('isLoading'); expect(cancelBtn.disabled).toBeTruthy(); - }); - }); + })); - it('should before cancel observable work correctly', () => { - return new Promise(resolve => { + it('should before cancel observable work correctly', () => + new Promise(resolve => { const t1 = Date.now(); dialogService .confirm({ @@ -239,8 +233,7 @@ describe('DialogService', () => { expect(cancelBtn.className).toContain('isLoading'); expect(cancelBtn.disabled).toBeTruthy(); - }); - }); + })); it('should before confirm callback could prevent close event', fakeAsync(() => { dialogService diff --git a/src/dialog/utils/index.ts b/src/dialog/utils/index.ts index 68b548a1c..4a2c52d66 100644 --- a/src/dialog/utils/index.ts +++ b/src/dialog/utils/index.ts @@ -16,3 +16,5 @@ export function getClosestDialog( ? openDialogs.find(dialog => dialog.id === parent.dataset.id) : null; } + +export * from './errors'; diff --git a/src/dropdown/dropdown-button/dropdown-button.component.ts b/src/dropdown/dropdown-button/dropdown-button.component.ts index 67ce79ae5..78924ea77 100644 --- a/src/dropdown/dropdown-button/dropdown-button.component.ts +++ b/src/dropdown/dropdown-button/dropdown-button.component.ts @@ -10,8 +10,7 @@ import { import { ButtonType } from '../../button/public-api'; import { ComponentSize } from '../../types'; -import { Bem, buildBem } from '../../utils/bem'; -import { coerceAttrBoolean } from '../../utils/coercion'; +import { Bem, buildBem, coerceAttrBoolean } from '../../utils'; import { MenuComponent } from '../menu/menu.component'; @Component({ diff --git a/src/dropdown/helper-directives.ts b/src/dropdown/helper-directives.ts index 29d7e4b6c..a4e177c6e 100644 --- a/src/dropdown/helper-directives.ts +++ b/src/dropdown/helper-directives.ts @@ -7,5 +7,4 @@ import { Directive } from '@angular/core'; '[class.aui-menu-group__title]': 'true', }, }) -// eslint-disable-next-line @typescript-eslint/no-extraneous-class export class MenuGroupTitleDirective {} diff --git a/src/dropdown/menu-item/menu-item.component.ts b/src/dropdown/menu-item/menu-item.component.ts index 7f3f6e7e8..1f95b0c9d 100644 --- a/src/dropdown/menu-item/menu-item.component.ts +++ b/src/dropdown/menu-item/menu-item.component.ts @@ -5,7 +5,7 @@ import { ViewEncapsulation, } from '@angular/core'; -import { Bem, buildBem } from '../../utils/bem'; +import { Bem, buildBem } from '../../utils'; import { MenuItemType } from '../dropdown.types'; @Component({ diff --git a/src/dropdown/menu/menu.component.ts b/src/dropdown/menu/menu.component.ts index cdcd98873..b4bcff8b2 100644 --- a/src/dropdown/menu/menu.component.ts +++ b/src/dropdown/menu/menu.component.ts @@ -11,7 +11,7 @@ import { } from '@angular/core'; import { ComponentSize } from '../../types'; -import { Bem, buildBem } from '../../utils/bem'; +import { Bem, buildBem } from '../../utils'; import { MenuItemComponent } from '../menu-item/menu-item.component'; import { MenuContentDirective } from './menu-content.directive'; diff --git a/src/dropdown/submenu/submenu.component.ts b/src/dropdown/submenu/submenu.component.ts index f32d49618..99c6fed1c 100644 --- a/src/dropdown/submenu/submenu.component.ts +++ b/src/dropdown/submenu/submenu.component.ts @@ -9,7 +9,7 @@ import { import { TooltipTrigger } from '../../tooltip/public-api'; import { ComponentSize } from '../../types'; -import { Bem, buildBem } from '../../utils/bem'; +import { Bem, buildBem } from '../../utils'; import { MenuItemComponent } from '../menu-item/menu-item.component'; @Component({ diff --git a/src/form/common-form.ts b/src/form/common-form.ts index 32ec6f2a5..048401210 100644 --- a/src/form/common-form.ts +++ b/src/form/common-form.ts @@ -9,7 +9,7 @@ import { ControlValueAccessor } from '@angular/forms'; import { Observable, ReplaySubject } from 'rxjs'; import { publishReplay, refCount, tap } from 'rxjs/operators'; -import { coerceAttrBoolean } from '../utils/coercion'; +import { coerceAttrBoolean } from '../utils'; /** * form control 内部不保持 value 属性,当不使用 ngModel 时可以通过 [(value)] 实现双向绑定; diff --git a/src/form/form-item/form-item.component.ts b/src/form/form-item/form-item.component.ts index f5b9909f9..3640bae51 100644 --- a/src/form/form-item/form-item.component.ts +++ b/src/form/form-item/form-item.component.ts @@ -22,7 +22,7 @@ import { takeUntil, } from 'rxjs/operators'; -import { Bem, buildBem } from '../../utils/bem'; +import { Bem, buildBem } from '../../utils'; import { FormDirective } from '../form.directive'; import { LabelPosition } from '../form.types'; import { diff --git a/src/form/helper-directives.ts b/src/form/helper-directives.ts index 0dd5bf0a7..a7a2e00ab 100644 --- a/src/form/helper-directives.ts +++ b/src/form/helper-directives.ts @@ -1,8 +1,7 @@ -/* eslint-disable @typescript-eslint/no-extraneous-class */ // tslint:disable: no-host-metadata-property import { Directive, Input } from '@angular/core'; -import { coerceAttrBoolean } from '../utils/coercion'; +import { coerceAttrBoolean } from '../utils'; @Directive({ selector: 'label[auiFormItemLabel]', diff --git a/src/icon/icon-register.service.ts b/src/icon/icon-register.service.ts index 085d9c3a7..50c68a2fa 100644 --- a/src/icon/icon-register.service.ts +++ b/src/icon/icon-register.service.ts @@ -6,7 +6,7 @@ import { auiIcons } from './icons'; import { getAuiIconFailedToLoadCustomIconFile, getAuiIconNoHttpProviderError, -} from './utils/errors'; +} from './utils'; @Injectable() export class IconRegisterService { diff --git a/src/icon/icon.component.html b/src/icon/icon.component.html index 714a57db3..1a4289e21 100644 --- a/src/icon/icon.component.html +++ b/src/icon/icon.component.html @@ -15,7 +15,7 @@ x2="150" y2="180" > - + - + diff --git a/src/icon/icon.component.ts b/src/icon/icon.component.ts index b4756417f..1656b67f5 100644 --- a/src/icon/icon.component.ts +++ b/src/icon/icon.component.ts @@ -5,7 +5,7 @@ import { ViewEncapsulation, } from '@angular/core'; -import { Bem, buildBem } from '../utils/bem'; +import { Bem, buildBem } from '../utils'; import { IconRegisterService } from './icon-register.service'; diff --git a/src/icon/utils/errors.ts b/src/icon/utils.ts similarity index 100% rename from src/icon/utils/errors.ts rename to src/icon/utils.ts diff --git a/src/inline-alert/helper-directives.ts b/src/inline-alert/helper-directives.ts index e211d4f2d..aa0ec53ce 100644 --- a/src/inline-alert/helper-directives.ts +++ b/src/inline-alert/helper-directives.ts @@ -4,5 +4,4 @@ import { Directive } from '@angular/core'; selector: '[auiInlineAlertTitle]', exportAs: 'auiInlineAlertTitle', }) -// eslint-disable-next-line @typescript-eslint/no-extraneous-class export class InlineAlertTitleDirective {} diff --git a/src/inline-alert/inline-alert.component.spec.ts b/src/inline-alert/inline-alert.component.spec.ts index f424657c3..64a16ee40 100644 --- a/src/inline-alert/inline-alert.component.spec.ts +++ b/src/inline-alert/inline-alert.component.spec.ts @@ -50,8 +50,8 @@ describe('InlineAlertComponent', () => { ).toBeNull(); }); - it('should emit close event when click close button', () => { - return new Promise(resolve => { + it('should emit close event when click close button', () => + new Promise(resolve => { ins.inlineAlertRef.close.subscribe(() => { resolve(); }); @@ -59,8 +59,7 @@ describe('InlineAlertComponent', () => { By.css('#inline-alert .aui-inline-alert__close'), ).nativeElement as HTMLElement).click(); fixture.detectChanges(); - }); - }); + })); it('should hide close button when not closable', () => { ins.closable = true; diff --git a/src/inline-alert/inline-alert.component.ts b/src/inline-alert/inline-alert.component.ts index 584650c99..1589e8181 100644 --- a/src/inline-alert/inline-alert.component.ts +++ b/src/inline-alert/inline-alert.component.ts @@ -8,7 +8,7 @@ import { ViewEncapsulation, } from '@angular/core'; -import { Bem, buildBem } from '../utils/bem'; +import { Bem, buildBem } from '../utils'; import { InlineAlertTitleDirective } from './helper-directives'; import { iconMap } from './icon-map'; diff --git a/src/input/helper-directives.ts b/src/input/helper-directives.ts index 0b44ffedb..45b6f0649 100644 --- a/src/input/helper-directives.ts +++ b/src/input/helper-directives.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-extraneous-class */ import { Directive } from '@angular/core'; @Directive({ diff --git a/src/input/input-group/__snapshots__/input-group.component.spec.ts.snap b/src/input/input-group/__snapshots__/input-group.component.spec.ts.snap index b2a6a92e2..7c9903fa5 100644 --- a/src/input/input-group/__snapshots__/input-group.component.spec.ts.snap +++ b/src/input/input-group/__snapshots__/input-group.component.spec.ts.snap @@ -70,7 +70,7 @@ exports[`InputGroupComponent should match snapshot 1`] = ` > { expect(inputEl.value).toBe('keyword'); })); - it('should emit change event when user input', () => { - return new Promise(resolve => { + it('should emit change event when user input', () => + new Promise(resolve => { ins.searchRef.keywordChange.subscribe(($event: string) => { expect($event).toBe('text'); resolve(); @@ -119,8 +119,7 @@ describe('SearchComponent', () => { inputEl.value = 'text'; inputEl.dispatchEvent(new Event('input')); fixture.detectChanges(); - }); - }); + })); it('should input element be disabled', fakeAsync(() => { ins.disabled = true; @@ -130,8 +129,8 @@ describe('SearchComponent', () => { expect(inputEl.disabled).toBeTruthy(); })); - it('should emit clear event', () => { - return new Promise(resolve => { + it('should emit clear event', () => + new Promise(resolve => { ins.searchRef.clear.subscribe(() => { resolve(); }); @@ -142,8 +141,7 @@ describe('SearchComponent', () => { fixture.detectChanges(); el.querySelector('.aui-search__clear').dispatchEvent(new Event('click')); expect(ins.searchRef.keyword).toBe(''); - }); - }); + })); it('should not emit clear event when disabled', () => { ins.searchRef.clear.subscribe(() => { @@ -159,8 +157,8 @@ describe('SearchComponent', () => { expect(ins.keyword).toBe('text'); }); - it('should emit search event when click search button', () => { - return new Promise(resolve => { + it('should emit search event when click search button', () => + new Promise(resolve => { ins.searchRef.search.subscribe(($event: string) => { expect($event).toBe('text'); resolve(); @@ -171,8 +169,7 @@ describe('SearchComponent', () => { ins.searchButton = true; fixture.detectChanges(); el.querySelector('.aui-search__button').dispatchEvent(new Event('click')); - }); - }); + })); it('should not emit search event when click search button', () => { ins.searchRef.search.subscribe(() => { @@ -186,8 +183,8 @@ describe('SearchComponent', () => { el.querySelector('.aui-search__button').dispatchEvent(new Event('click')); }); - it('should emit search event when press enter', () => { - return new Promise(resolve => { + it('should emit search event when press enter', () => + new Promise(resolve => { ins.searchRef.search.subscribe(($event: string) => { expect($event).toBe('text'); resolve(); @@ -198,8 +195,7 @@ describe('SearchComponent', () => { ins.searchButton = true; fixture.detectChanges(); inputEl.dispatchEvent(new KeyboardEvent('keyup', { key: 'enter' })); - }); - }); + })); }); @Component({ diff --git a/src/input/search/search.component.ts b/src/input/search/search.component.ts index e9aa9bad1..ff780b3c9 100644 --- a/src/input/search/search.component.ts +++ b/src/input/search/search.component.ts @@ -8,7 +8,7 @@ import { } from '@angular/core'; import { ComponentSize } from '../../types'; -import { Bem, buildBem } from '../../utils/bem'; +import { Bem, buildBem } from '../../utils'; @Component({ selector: 'aui-search', diff --git a/src/input/tags-input/mixin.scss b/src/input/tags-input/mixin.scss index fd08b96f0..615b69288 100644 --- a/src/input/tags-input/mixin.scss +++ b/src/input/tags-input/mixin.scss @@ -146,3 +146,10 @@ } } } + +@mixin with-height-limit { + @include scroll-bar; + + overflow: auto; + position: static; +} diff --git a/src/input/tags-input/tags-input.component.html b/src/input/tags-input/tags-input.component.html index fc9e4a052..4492d1754 100644 --- a/src/input/tags-input/tags-input.component.html +++ b/src/input/tags-input/tags-input.component.html @@ -2,6 +2,7 @@ [class]="rootClass" (mousedown)="$event.preventDefault()" (click)="inputRef.focus()" + [style.max-height]="maxHeight" > 1: for ```tagClassFn``` maybe affect lineHeight + _inputValidator: ValidatorFn; _inputAsyncValidator: AsyncValidatorFn; @@ -101,6 +110,22 @@ export class TagsInputComponent @ViewChild('inputValueMirror', { static: true }) inputValueMirror: ElementRef; + @HostBinding('style.position') + get hostPosition() { + return this.withMaxRowCount.hostPosition(); + } + + @HostBinding('style.display') + get hostDisplay() { + return this.withMaxRowCount.hostDisplay(); + } + + get maxHeight() { + return this.withMaxRowCount.maxHeight(); + } + + private readonly withMaxRowCount = createWithMaxRowCount(this); + snapshot = { value: [] as string[], }; @@ -128,7 +153,7 @@ export class TagsInputComponent this.disabled ? 'isDisabled' : '' } ${this.focused ? 'isFocused' : ''} ${ this.clearable ? 'isClearable' : '' - }`; + } ${this.maxRowCount > 0 ? 'withHeightLimit' : ''}`; } get tagSize() { diff --git a/src/input/tags-input/with-max-row-count.ts b/src/input/tags-input/with-max-row-count.ts new file mode 100644 index 000000000..be6d33071 --- /dev/null +++ b/src/input/tags-input/with-max-row-count.ts @@ -0,0 +1,56 @@ +import { ComponentSize } from '../../types'; + +function getRowHeight(size: ComponentSize) { + switch (size) { + case ComponentSize.Large: + return 33; + case ComponentSize.Small: + return 23; + case ComponentSize.Mini: + return 21; + default: + return 25; + } +} + +function getContainerPaddingPatch(size: ComponentSize) { + switch (size) { + case ComponentSize.Mini: + return 3; + case ComponentSize.Small: + return 5; + default: + return 7; + } +} + +export function createWithMaxRowCount< + T extends { + maxRowCount: number; + customRowHeight?: number; + size: ComponentSize; + } +>(component: T) { + const hostPosition = () => (component.maxRowCount > 0 ? 'relative' : null); + const hostDisplay = () => (component.maxRowCount ? 'block' : null); + const maxHeight = () => { + if (component.maxRowCount <= 0) { + return null; + } + + const paddingPatch = getContainerPaddingPatch(component.size); + + const rowHeight = + isNaN(component.customRowHeight) || component.customRowHeight <= 0 + ? getRowHeight(component.size) + : component.customRowHeight; + + return `${rowHeight * component.maxRowCount + paddingPatch}px`; + }; + + return { + hostPosition, + hostDisplay, + maxHeight, + }; +} diff --git a/src/message/message.component.ts b/src/message/message.component.ts index d1434efd8..685f8dbdc 100644 --- a/src/message/message.component.ts +++ b/src/message/message.component.ts @@ -9,7 +9,7 @@ import { } from '@angular/core'; import { Subject } from 'rxjs'; -import { Bem, buildBem } from '../utils/bem'; +import { Bem, buildBem } from '../utils'; import { MessageAnimations } from './message-animations'; import { MessageConfig, MessageType } from './message.config'; diff --git a/src/nav-menu/nav-menu.component.ts b/src/nav-menu/nav-menu.component.ts index f96415188..a868b3870 100644 --- a/src/nav-menu/nav-menu.component.ts +++ b/src/nav-menu/nav-menu.component.ts @@ -127,14 +127,14 @@ export class NavMenuComponent implements AfterContentInit { bufferCount(2, 1), startWith([null, null]), debounceTime(0), - switchMap(([prevKey, currKey]) => { - return currKey + switchMap(([prevKey, currKey]) => + currKey ? of(currKey) : this.secondaryPanelHover$$.pipe( debounceTime(0), switchMap(hover => (hover ? of(prevKey) : of(null))), - ); - }), + ), + ), ) .pipe( switchMap(key => diff --git a/src/notification/notification.component.ts b/src/notification/notification.component.ts index 36c8fb2ef..e0e0c4346 100644 --- a/src/notification/notification.component.ts +++ b/src/notification/notification.component.ts @@ -16,7 +16,7 @@ import { import { MessageAnimations } from '../message/message-animations'; import { MessageComponent } from '../message/message.component'; -import { Bem, buildBem } from '../utils/bem'; +import { Bem, buildBem } from '../utils'; import { NotificationConfig } from './notification.config'; diff --git a/src/notification/public-api.ts b/src/notification/public-api.ts index e3fa4ffe7..5a34986a5 100644 --- a/src/notification/public-api.ts +++ b/src/notification/public-api.ts @@ -1,3 +1,4 @@ export * from './notification.module'; +export * from './notification.component'; export * from './notification.service'; export * from './notification.config'; diff --git a/src/paginator/__snapshots__/paginator.component.spec.ts.snap b/src/paginator/__snapshots__/paginator.component.spec.ts.snap index 5e57e783c..eb431c36e 100644 --- a/src/paginator/__snapshots__/paginator.component.spec.ts.snap +++ b/src/paginator/__snapshots__/paginator.component.spec.ts.snap @@ -184,6 +184,8 @@ exports[`PaginatorComponent should change page size correctly 1`] = ` class="aui-select__label" > + + 100 @@ -512,6 +514,8 @@ exports[`PaginatorComponent should render current template 1`] = ` class="aui-select__label" > + + 20 @@ -851,6 +855,8 @@ exports[`PaginatorComponent should render current template 2`] = ` class="aui-select__label" > + + 20 diff --git a/src/paginator/paginator-intl.ts b/src/paginator/paginator-intl.ts index ff3e4de52..baab8beeb 100644 --- a/src/paginator/paginator-intl.ts +++ b/src/paginator/paginator-intl.ts @@ -19,9 +19,7 @@ export class PaginatorIntl { jumperLabelPrefix = 'Goto'; jumperLabelSuffix = ''; - getTotalLabel = (length: number) => { - return `Total ${length}`; - }; + getTotalLabel = (length: number) => `Total ${length}`; } export function PAGINATOR_INTL_PROVIDER_FACTORY(parentIntl: PaginatorIntl) { diff --git a/src/public-api.ts b/src/public-api.ts index f308de970..6514716f9 100644 --- a/src/public-api.ts +++ b/src/public-api.ts @@ -1,4 +1,5 @@ export * from './types'; +export * from './utils'; export * from './table/public-api'; export * from './sort/public-api'; export * from './button/public-api'; @@ -30,3 +31,5 @@ export * from './color-picker/public-api'; export * from './accordion/public-api'; export * from './page/public-api'; export * from './scrolling/public-api'; +export * from './theme/public-api'; +export * from './anchor/public-api'; diff --git a/src/radio/radio-button/radio-button.component.ts b/src/radio/radio-button/radio-button.component.ts index 3e9f28432..c2752e751 100644 --- a/src/radio/radio-button/radio-button.component.ts +++ b/src/radio/radio-button/radio-button.component.ts @@ -8,7 +8,7 @@ import { } from '@angular/core'; import { takeUntil } from 'rxjs/operators'; -import { Bem, buildBem } from '../../utils/bem'; +import { Bem, buildBem } from '../../utils'; import { BaseRadio } from '../base-radio'; import { RadioGroupComponent } from '../radio-group/radio-group.component'; import { RadioSize } from '../radio.types'; diff --git a/src/radio/radio.component.ts b/src/radio/radio.component.ts index 3359196b1..9dabb39e4 100644 --- a/src/radio/radio.component.ts +++ b/src/radio/radio.component.ts @@ -6,7 +6,7 @@ import { ViewEncapsulation, } from '@angular/core'; -import { Bem, buildBem } from '../utils/bem'; +import { Bem, buildBem } from '../utils'; import { BaseRadio } from './base-radio'; import { RadioGroupComponent } from './radio-group/radio-group.component'; diff --git a/src/select/base-select.ts b/src/select/base-select.ts index 1fe3aa77e..cabb471a9 100644 --- a/src/select/base-select.ts +++ b/src/select/base-select.ts @@ -9,7 +9,6 @@ import { OnDestroy, Output, QueryList, - TemplateRef, ViewChild, ViewChildren, } from '@angular/core'; @@ -25,8 +24,12 @@ import { import { CommonFormControl } from '../form/public-api'; import { TooltipDirective } from '../tooltip/public-api'; import { ComponentSize } from '../types'; -import { coerceAttrBoolean, coerceString } from '../utils/coercion'; -import { scrollIntoView } from '../utils/scroll-into-view'; +import { + coerceAttrBoolean, + coerceString, + isTemplateRef, + scrollIntoView, +} from '../utils'; import { OptionComponent } from './option/option.component'; import { OptionFilterFn, SelectFilterOption, TrackFn } from './select.types'; @@ -148,6 +151,8 @@ export abstract class BaseSelect @ContentChildren(OptionComponent, { descendants: true }) contentOptions: QueryList>; + isTemplateRef = isTemplateRef; + /** * Utility field to make sure the users always see the value as type array */ @@ -332,12 +337,6 @@ export abstract class BaseSelect this.selectOption(option); } - isTemplate( - label: string | TemplateRef, - ): label is TemplateRef { - return label instanceof TemplateRef; - } - protected autoFocusFirstOption() { if (this.defaultFirstOption && this.allowCreate && this.filterString) { const matchedOption = this.contentOptions.find( diff --git a/src/select/helper-directives.ts b/src/select/helper-directives.ts index d018c9a1d..94f66eb9e 100644 --- a/src/select/helper-directives.ts +++ b/src/select/helper-directives.ts @@ -7,5 +7,4 @@ import { Directive } from '@angular/core'; '[class.aui-option-group__title]': 'true', }, }) -// eslint-disable-next-line @typescript-eslint/no-extraneous-class export class OptionGroupTitleDirective {} diff --git a/src/select/multi-select/multi-select.component.html b/src/select/multi-select/multi-select.component.html index 5e4d07643..87ef89d53 100644 --- a/src/select/multi-select/multi-select.component.html +++ b/src/select/multi-select/multi-select.component.html @@ -10,6 +10,7 @@ (auiTooltipShow)="onShowOptions()" (auiTooltipHide)="onHideOptions()" (mousedown)="$event.preventDefault()" + [style.max-height]="maxHeight" >
diff --git a/src/select/multi-select/multi-select.component.scss b/src/select/multi-select/multi-select.component.scss index d928a1c4e..98123e0c3 100644 --- a/src/select/multi-select/multi-select.component.scss +++ b/src/select/multi-select/multi-select.component.scss @@ -93,6 +93,10 @@ $block: aui-multi-select; cursor: default; } + &.withHeightLimit { + @include with-height-limit; + } + &:not(.isDisabled) .aui-icon-caret_down_s { fill: $input-color; } diff --git a/src/select/multi-select/multi-select.component.spec.ts b/src/select/multi-select/multi-select.component.spec.ts index 1a15aaa7e..60b3e5625 100644 --- a/src/select/multi-select/multi-select.component.spec.ts +++ b/src/select/multi-select/multi-select.component.spec.ts @@ -148,7 +148,5 @@ class TestComponent { @ViewChild('selectRef', { static: true }) selectRef: MultiSelectComponent; - tagClassFn = (_: string, tag: string) => { - return `tag-${tag}`; - }; + tagClassFn = (_: string, tag: string) => `tag-${tag}`; } diff --git a/src/select/multi-select/multi-select.component.ts b/src/select/multi-select/multi-select.component.ts index eae2664cb..697abe3b3 100644 --- a/src/select/multi-select/multi-select.component.ts +++ b/src/select/multi-select/multi-select.component.ts @@ -4,6 +4,7 @@ import { ChangeDetectorRef, Component, ElementRef, + HostBinding, Input, QueryList, Renderer2, @@ -22,9 +23,9 @@ import { takeUntil, } from 'rxjs/operators'; +import { createWithMaxRowCount } from '../../input/tags-input/with-max-row-count'; import { ComponentSize } from '../../types'; -import { Bem, buildBem } from '../../utils/bem'; -import { coerceString } from '../../utils/coercion'; +import { Bem, buildBem, coerceString } from '../../utils'; import { BaseSelect } from '../base-select'; import { OptionComponent } from '../option/option.component'; import { @@ -66,7 +67,13 @@ export class MultiSelectComponent values$ = this.value$$.asObservable(); @Input() - tagClassFn: TagClassFn; + tagClassFn: TagClassFn; + + @Input() + maxRowCount = 0; + + @Input() + customRowHeight = 0; // 0: use default style const value, > 1: for ```tagClassFn``` maybe affect lineHeight @ViewChild('inputRef', { static: true }) inputRef: ElementRef; @@ -74,6 +81,16 @@ export class MultiSelectComponent @ViewChild('inputValueMirror', { static: true }) inputValueMirror: ElementRef; + @HostBinding('style.position') + get hostPosition() { + return this.withMaxRowCount.hostPosition(); + } + + @HostBinding('style.display') + get hostDisplay() { + return this.withMaxRowCount.hostDisplay(); + } + inputValue = ''; get rootClass() { @@ -82,7 +99,7 @@ export class MultiSelectComponent this.disabled ? 'isDisabled' : '' } ${this.focused ? 'isFocused' : ''} ${ this.displayClearBtn ? 'isClearable' : '' - }`; + } ${this.maxRowCount > 0 ? 'withHeightLimit' : ''}`; } get tagSize() { @@ -101,8 +118,15 @@ export class MultiSelectComponent return !this.disabled && this.clearable && this.selectedValues.length; } + get maxHeight() { + return this.withMaxRowCount.maxHeight(); + } + + private readonly withMaxRowCount = createWithMaxRowCount(this); + focused = false; - trackByValue = (_: number, item: OptionComponent) => + + trackByValue = (_: number, item: SelectFilterOption) => this.trackFn(item.value); constructor(cdr: ChangeDetectorRef, private readonly renderer: Renderer2) { diff --git a/src/select/option/option.component.ts b/src/select/option/option.component.ts index 3e958a50a..d42814e2c 100644 --- a/src/select/option/option.component.ts +++ b/src/select/option/option.component.ts @@ -20,8 +20,7 @@ import { } from 'rxjs/operators'; import { ComponentSize } from '../../types'; -import { Bem, buildBem } from '../../utils/bem'; -import { coerceAttrBoolean } from '../../utils/coercion'; +import { Bem, buildBem, coerceAttrBoolean } from '../../utils'; import { BaseSelect } from '../base-select'; import { MultiSelectComponent } from '../multi-select/multi-select.component'; diff --git a/src/select/select.component.html b/src/select/select.component.html index 0e4c686de..8628c5561 100644 --- a/src/select/select.component.html +++ b/src/select/select.component.html @@ -46,12 +46,14 @@ [attr.disabled]="disabled ? true : null" >
- - {{ (selectedOption$ | async).label }} + + + {{ optionLabel }} +
diff --git a/src/select/select.component.ts b/src/select/select.component.ts index 35099ab26..a4d6c7bed 100644 --- a/src/select/select.component.ts +++ b/src/select/select.component.ts @@ -19,7 +19,7 @@ import { } from 'rxjs/operators'; import { InputComponent } from '../input/public-api'; -import { coerceString } from '../utils/coercion'; +import { coerceString } from '../utils'; import { BaseSelect } from './base-select'; import { OptionComponent } from './option/option.component'; @@ -60,9 +60,8 @@ export class SelectComponent return `aui-select aui-select--${this.size}`; } - isClearable = (hasSelected: boolean) => { - return !this.disabled && this.clearable && hasSelected; - }; + isClearable = (hasSelected: boolean) => + !this.disabled && this.clearable && hasSelected; ngAfterContentInit() { super.ngAfterContentInit(); diff --git a/src/select/select.types.ts b/src/select/select.types.ts index 2cb659034..c130f49dc 100644 --- a/src/select/select.types.ts +++ b/src/select/select.types.ts @@ -18,8 +18,11 @@ export type OptionFilterFn = ( export type TrackFn = (value: T) => R; -export type TagClassFn = ( - label: string, - value: unknown, +export type TagClassFn< + V, + T extends string | TemplateRef = string | TemplateRef +> = ( + label: T, + value: V, ) => // tslint:disable-next-line: max-union-size string | string[] | Set | { [className: string]: unknown }; diff --git a/src/select/validators.ts b/src/select/validators.ts index 9ff52c7d2..2603cf4fe 100644 --- a/src/select/validators.ts +++ b/src/select/validators.ts @@ -7,7 +7,7 @@ import { ValidatorFn, } from '@angular/forms'; -import { coerceAttrBoolean } from '../utils/coercion'; +import { coerceAttrBoolean } from '../utils'; import { SelectComponent } from './select.component'; import { TrackFn } from './select.types'; @@ -19,13 +19,12 @@ export class AuiSelectValidators { options: T[], trackFn: TrackFn = val => val, ): ValidatorFn { - return (control: AbstractControl) => { - return options.some(option => trackFn(option) === trackFn(control.value)) + return (control: AbstractControl) => + options.some(option => trackFn(option) === trackFn(control.value)) ? null : { includes: control.value, }; - }; } } diff --git a/src/sort/sort-header.component.ts b/src/sort/sort-header.component.ts index c162dfe34..04da5abd9 100644 --- a/src/sort/sort-header.component.ts +++ b/src/sort/sort-header.component.ts @@ -15,7 +15,7 @@ import { Subscription, merge } from 'rxjs'; * TODO: prevent ```public property '' of exported class has or is using name '' * from external module '' but cannot be named``` issue. */ -import { Bem, buildBem } from '../utils/bem'; +import { Bem, buildBem } from '../utils'; import { getSortHeaderNotContainedWithinSortError } from './sort-errors'; import { SortDirective } from './sort.directive'; diff --git a/src/status-bar/status-bar.component.scss b/src/status-bar/status-bar.component.scss index 505e207f6..e1a257618 100644 --- a/src/status-bar/status-bar.component.scss +++ b/src/status-bar/status-bar.component.scss @@ -24,19 +24,19 @@ $block: aui-status-bar; flex: 1 1 1; &--primary { - background-color: mix($color-white, $color-primary, 20%); + background-color: $color-primary-lighter; } &--success { - background-color: mix($color-white, $color-success, 20%); + background-color: $color-success-lighter; } &--warning { - background-color: mix($color-white, $color-warning, 20%); + background-color: $color-warning-lighter; } &--error { - background-color: mix($color-white, $color-error, 20%); + background-color: $color-error-lighter; } &--info { - background-color: mix($color-white, $color-info, 20%); + background-color: $color-info-lighter; } &--pending { height: $status-bar-pending-gradient-width * 8; diff --git a/src/status-bar/status-bar.component.ts b/src/status-bar/status-bar.component.ts index e5ee7ee88..6c105e080 100644 --- a/src/status-bar/status-bar.component.ts +++ b/src/status-bar/status-bar.component.ts @@ -8,7 +8,7 @@ import { ViewEncapsulation, } from '@angular/core'; -import { Bem, buildBem } from '../utils/bem'; +import { Bem, buildBem } from '../utils'; import { Status, StatusBarSize } from './status-bar.types'; diff --git a/src/switch/switch.component.ts b/src/switch/switch.component.ts index 4626365f1..58bb4b47c 100644 --- a/src/switch/switch.component.ts +++ b/src/switch/switch.component.ts @@ -8,7 +8,7 @@ import { import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { CommonFormControl } from '../form/public-api'; -import { Bem, buildBem } from '../utils/bem'; +import { Bem, buildBem } from '../utils'; const prefix = 'aui-switch'; diff --git a/src/table-of-contents/toc-container.directive.ts b/src/table-of-contents/toc-container.directive.ts index cc5769045..772bb6409 100644 --- a/src/table-of-contents/toc-container.directive.ts +++ b/src/table-of-contents/toc-container.directive.ts @@ -109,14 +109,13 @@ export class TocContainerDirective implements AfterContentInit, OnDestroy { .pipe( startWith(this.scrollTop), debounceTime(200), - map(scrollTop => { - return this._contents.reduce( - // eslint-disable-next-line unicorn/no-fn-reference-in-iterator + map(scrollTop => + this._contents.reduce( this.isScrollEnd ? this.getMaxContent.bind(this) : this.getMinContent(scrollTop), - ); - }), + ), + ), map(actived => actived.auiTocContent), ) .pipe( diff --git a/src/table/table-cell.directive.ts b/src/table/table-cell.directive.ts index c0a1bb1b4..7a0182f5f 100644 --- a/src/table/table-cell.directive.ts +++ b/src/table/table-cell.directive.ts @@ -1,7 +1,7 @@ import { CdkCell, CdkColumnDef } from '@angular/cdk/table'; import { Directive, ElementRef } from '@angular/core'; -import { buildBem } from '../utils/bem'; +import { buildBem } from '../utils'; const bem = buildBem('aui-table'); diff --git a/src/table/table-header-cell.directive.ts b/src/table/table-header-cell.directive.ts index 3cfca802a..4dfbcac62 100644 --- a/src/table/table-header-cell.directive.ts +++ b/src/table/table-header-cell.directive.ts @@ -1,7 +1,7 @@ import { CdkColumnDef, CdkHeaderCell } from '@angular/cdk/table'; import { Directive, ElementRef } from '@angular/core'; -import { buildBem } from '../utils/bem'; +import { buildBem } from '../utils'; const bem = buildBem('aui-table'); diff --git a/src/table/table.spec.ts b/src/table/table.spec.ts index b5a1802d6..3f293bde1 100644 --- a/src/table/table.spec.ts +++ b/src/table/table.spec.ts @@ -3,7 +3,7 @@ import { Component, ViewChild } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { BehaviorSubject, Observable } from 'rxjs'; -import { buildBem } from '../utils/bem'; +import { buildBem } from '../utils'; import { TableComponent, TableModule } from './public-api'; diff --git a/src/tabs/tab-directives.ts b/src/tabs/tab-directives.ts index cd4551306..ae4a35a73 100644 --- a/src/tabs/tab-directives.ts +++ b/src/tabs/tab-directives.ts @@ -11,7 +11,7 @@ import { } from '@angular/core'; import { Subscription } from 'rxjs'; -import { coerceAttrBoolean } from '../utils/coercion'; +import { coerceAttrBoolean } from '../utils'; import { TabBodyComponent } from './tab-body.component'; diff --git a/src/tabs/tab-group.component.ts b/src/tabs/tab-group.component.ts index c7204662f..5d86bda33 100644 --- a/src/tabs/tab-group.component.ts +++ b/src/tabs/tab-group.component.ts @@ -19,8 +19,7 @@ import { } from '@angular/core'; import { Subject, Subscription, merge } from 'rxjs'; -import { Bem, buildBem } from '../utils/bem'; -import { coerceNumber } from '../utils/coercion'; +import { Bem, buildBem, coerceNumber } from '../utils'; import { TabHeaderAddonDirective } from './tab-directives'; import { TabHeaderComponent } from './tab-header.component'; diff --git a/src/tabs/tab-header-active-indicator.component.ts b/src/tabs/tab-header-active-indicator.component.ts index b8b5d95c0..9399c5a7d 100644 --- a/src/tabs/tab-header-active-indicator.component.ts +++ b/src/tabs/tab-header-active-indicator.component.ts @@ -7,7 +7,7 @@ import { ViewEncapsulation, } from '@angular/core'; -import { Bem, buildBem } from '../utils/bem'; +import { Bem, buildBem } from '../utils'; import { TabType } from './tabs.types'; diff --git a/src/tabs/tab-header.component.ts b/src/tabs/tab-header.component.ts index 06e136469..c15d3add1 100644 --- a/src/tabs/tab-header.component.ts +++ b/src/tabs/tab-header.component.ts @@ -20,8 +20,7 @@ import { import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; -import { Bem, buildBem } from '../utils/bem'; -import { coerceNumber } from '../utils/coercion'; +import { Bem, buildBem, coerceNumber } from '../utils'; import { TabHeaderAddonDirective, @@ -182,7 +181,6 @@ export class TabHeaderComponent */ ngAfterContentInit() { const resize = this._viewportRuler.change(150); - // eslint-disable-next-line unicorn/consistent-function-scoping const realign = () => { this._updatePagination(); this._alignActiveIndicatorToSelectedTab(); diff --git a/src/tabs/tab.component.ts b/src/tabs/tab.component.ts index d120f0b79..02058a6df 100644 --- a/src/tabs/tab.component.ts +++ b/src/tabs/tab.component.ts @@ -18,7 +18,7 @@ import { } from '@angular/core'; import { Subject } from 'rxjs'; -import { coerceAttrBoolean } from '../utils/coercion'; +import { coerceAttrBoolean } from '../utils'; import { TabContentDirective, TabLabelDirective } from './tab-directives'; diff --git a/src/tag/check-tag/check-tag.component.ts b/src/tag/check-tag/check-tag.component.ts index afda93bc9..853de368c 100644 --- a/src/tag/check-tag/check-tag.component.ts +++ b/src/tag/check-tag/check-tag.component.ts @@ -8,7 +8,7 @@ import { } from '@angular/core'; import { ComponentSize } from '../../types'; -import { Bem, buildBem } from '../../utils/bem'; +import { Bem, buildBem } from '../../utils'; import { TagType } from '../tag.types'; @Component({ diff --git a/src/tag/tag.component.ts b/src/tag/tag.component.ts index ee62269fb..e44754c50 100644 --- a/src/tag/tag.component.ts +++ b/src/tag/tag.component.ts @@ -8,7 +8,7 @@ import { } from '@angular/core'; import { ComponentSize } from '../types'; -import { Bem, buildBem } from '../utils/bem'; +import { Bem, buildBem } from '../utils'; import { TagType } from './tag.types'; diff --git a/src/theme/_base-var.scss b/src/theme/_base-var.scss index 3a34307d9..8c57e93f4 100644 --- a/src/theme/_base-var.scss +++ b/src/theme/_base-var.scss @@ -1,13 +1,13 @@ // Colors -$color-white: #fff; -$color-black: #000; +$color-white: var(--aui-color-white); +$color-black: var(--aui-color-black); -$color-primary: #006eff; -$color-success: #0abf5b; -$color-warning: #ff9d00; -$color-danger: #e54545; -$color-error: #e54545; -$color-info: #ccc; +$color-primary: var(--aui-color-primary); +$color-success: var(--aui-color-success); +$color-warning: var(--aui-color-warning); +$color-danger: var(--aui-color-danger); +$color-error: var(--aui-color-error); +$color-info: var(--aui-color-info); $status-color-map: ( success: $color-success, @@ -16,48 +16,54 @@ $status-color-map: ( info: $color-primary, ); -$color-primary-light: mix($color-white, $color-primary, 60%); -$color-success-light: mix($color-white, $color-success, 60%); -$color-warning-light: mix($color-white, $color-warning, 60%); -$color-error-light: mix($color-white, $color-error, 60%); -$color-info-light: mix($color-white, $color-info, 60%); - -$color-primary-lighter: mix($color-white, $color-primary, 70%); -$color-success-lighter: mix($color-white, $color-success, 70%); -$color-warning-lighter: mix($color-white, $color-warning, 70%); -$color-error-lighter: mix($color-white, $color-error, 70%); -$color-info-lighter: mix($color-white, $color-info, 70%); - -$color-primary-extra-light: mix($color-white, $color-primary, 90%); -$color-success-extra-light: mix($color-white, $color-success, 90%); -$color-warning-extra-light: mix($color-white, $color-warning, 90%); -$color-error-extra-light: mix($color-white, $color-error, 90%); -$color-info-extra-light: mix($color-white, $color-info, 90%); - -$color-primary-ultra-light: mix($color-white, $color-primary, 95%); -$color-success-ultra-light: mix($color-white, $color-success, 95%); -$color-warning-ultra-light: mix($color-white, $color-warning, 95%); -$color-error-ultra-light: mix($color-white, $color-error, 95%); -$color-info-ultra-light: mix($color-white, $color-info, 95%); - -$color-primary-dark: mix($color-black, $color-primary, 10%); -$color-success-dark: mix($color-black, $color-success, 10%); -$color-warning-dark: mix($color-black, $color-warning, 10%); -$color-error-dark: mix($color-black, $color-error, 10%); -$color-info-dark: mix($color-black, $color-info, 10%); - -$color-primary-darker: mix($color-black, $color-primary, 20%); -$color-success-darker: mix($color-black, $color-success, 20%); -$color-warning-darker: mix($color-black, $color-warning, 20%); -$color-error-darker: mix($color-black, $color-error, 20%); -$color-info-darker: mix($color-black, $color-info, 20%); - -$color-text-main: #333; -$color-text-secondary: #666; -$color-text-help: #999; -$color-text-placeholder: #ccc; - -$color-disabled-bg: #f2f2f2; +$color-primary-light: var(--aui-color-primary-light); +$color-success-light: var(--aui-color-success-light); +$color-warning-light: var(--aui-color-warning-light); +$color-error-light: var(--aui-color-error-light); +$color-danger-light: $color-error-light; +$color-info-light: var(--aui-color-info-light); + +$color-primary-lighter: var(--aui-color-primary-lighter); +$color-success-lighter: var(--aui-color-success-lighter); +$color-warning-lighter: var(--aui-color-warning-lighter); +$color-error-lighter: var(--aui-color-error-lighter); +$color-danger-lighter: $color-error-lighter; +$color-info-lighter: var(--aui-color-info-lighter); + +$color-primary-extra-light: var(--aui-color-primary-extra-light); +$color-success-extra-light: var(--aui-color-success-extra-light); +$color-warning-extra-light: var(--aui-color-warning-extra-light); +$color-error-extra-light: var(--aui-color-error-extra-light); +$color-danger-extra-light: $color-error-extra-light; +$color-info-extra-light: var(--aui-color-info-extra-light); + +$color-primary-ultra-light: var(--aui-color-primary-ultra-light); +$color-success-ultra-light: var(--aui-color-success-ultra-light); +$color-warning-ultra-light: var(--aui-color-warning-ultra-light); +$color-error-ultra-light: var(--aui-color-error-ultra-light); +$color-danger-ultra-light: $color-error-ultra-light; +$color-info-ultra-light: var(--aui-color-info-ultra-light); + +$color-primary-dark: var(--aui-color-primary-dark); +$color-success-dark: var(--aui-color-success-dark); +$color-warning-dark: var(--aui-color-warning-dark); +$color-error-dark: var(--aui-color-error-dark); +$color-danger-dark: $color-error-dark; +$color-info-dark: var(--aui-color-info-dark); + +$color-primary-darker: var(--aui-color-primary-darker); +$color-success-darker: var(--aui-color-success-darker); +$color-warning-darker: var(--aui-color-warning-darker); +$color-error-darker: var(--aui-color-error-darker); +$color-danger-darker: $color-error-darker; +$color-info-darker: var(--aui-color-info-darker); + +$color-text-main: var(--aui-color-text-main); +$color-text-secondary: var(--aui-color-text-secondary); +$color-text-help: var(--aui-color-text-help); +$color-text-placeholder: var(--aui-color-text-placeholder); + +$color-disabled-bg: var(--aui-color-disabled-bg); // Font $font-size-extra-large: 20px; @@ -89,11 +95,11 @@ $border-style-dashed: dashed; // Outline $outline-width: 3px; -$outline-color-primary: mix($color-white, $color-primary, 60%); -$outline-color-success: mix($color-white, $color-success, 60%); -$outline-color-warning: mix($color-white, $color-warning, 60%); -$outline-color-error: mix($color-white, $color-error, 60%); -$outline-color-info: mix($color-white, $color-info, 60%); +$outline-color-primary: var(--aui-color-outline-primary); +$outline-color-success: var(--aui-color-outline-success); +$outline-color-warning: var(--aui-color-outline-warning); +$outline-color-error: var(--aui-color-outline-error); +$outline-color-info: var(--aui-color-outline-info); // Animation $animation-duration: 0.3s; diff --git a/src/theme/_colors.scss b/src/theme/_colors.scss new file mode 100644 index 000000000..58dbd1f22 --- /dev/null +++ b/src/theme/_colors.scss @@ -0,0 +1,121 @@ +@mixin light-mode { + $color-white: #fff; + $color-black: #000; + + $color-primary: #006eff; + $color-success: #0abf5b; + $color-warning: #ff9d00; + $color-danger: #e54545; + $color-error: #e54545; + $color-info: #ccc; + + --aui-color-white: #{$color-white}; + --aui-color-black: #{$color-black}; + --aui-color-primary: #{$color-primary}; + --aui-color-success: #{$color-success}; + --aui-color-warning: #{$color-warning}; + --aui-color-danger: #{$color-danger}; + --aui-color-error: #{$color-error}; + --aui-color-info: #{$color-info}; + --aui-color-primary-light: #{mix($color-white, $color-primary, 60%)}; + --aui-color-success-light: #{mix($color-white, $color-success, 60%)}; + --aui-color-warning-light: #{mix($color-white, $color-warning, 60%)}; + --aui-color-error-light: #{mix($color-white, $color-error, 60%)}; + --aui-color-info-light: #{mix($color-white, $color-info, 60%)}; + --aui-color-primary-lighter: #{mix($color-white, $color-primary, 70%)}; + --aui-color-success-lighter: #{mix($color-white, $color-success, 70%)}; + --aui-color-warning-lighter: #{mix($color-white, $color-warning, 70%)}; + --aui-color-error-lighter: #{mix($color-white, $color-error, 70%)}; + --aui-color-info-lighter: #{mix($color-white, $color-info, 70%)}; + --aui-color-primary-extra-light: #{mix($color-white, $color-primary, 90%)}; + --aui-color-success-extra-light: #{mix($color-white, $color-success, 90%)}; + --aui-color-warning-extra-light: #{mix($color-white, $color-warning, 90%)}; + --aui-color-error-extra-light: #{mix($color-white, $color-error, 90%)}; + --aui-color-info-extra-light: #{mix($color-white, $color-info, 90%)}; + --aui-color-primary-ultra-light: #{mix($color-white, $color-primary, 95%)}; + --aui-color-success-ultra-light: #{mix($color-white, $color-success, 95%)}; + --aui-color-warning-ultra-light: #{mix($color-white, $color-warning, 95%)}; + --aui-color-error-ultra-light: #{mix($color-white, $color-error, 95%)}; + --aui-color-info-ultra-light: #{mix($color-white, $color-info, 95%)}; + --aui-color-primary-dark: #{mix($color-black, $color-primary, 10%)}; + --aui-color-success-dark: #{mix($color-black, $color-success, 10%)}; + --aui-color-warning-dark: #{mix($color-black, $color-warning, 10%)}; + --aui-color-error-dark: #{mix($color-black, $color-error, 10%)}; + --aui-color-info-dark: #{mix($color-black, $color-info, 10%)}; + --aui-color-primary-darker: #{mix($color-black, $color-primary, 20%)}; + --aui-color-success-darker: #{mix($color-black, $color-success, 20%)}; + --aui-color-warning-darker: #{mix($color-black, $color-warning, 20%)}; + --aui-color-error-darker: #{mix($color-black, $color-error, 20%)}; + --aui-color-info-darker: #{mix($color-black, $color-info, 20%)}; + --aui-color-text-main: #333; + --aui-color-text-secondary: #666; + --aui-color-text-help: #999; + --aui-color-text-placeholder: #ccc; + --aui-color-disabled-bg: #f2f2f2; + --aui-color-outline-primary: #{mix($color-white, $color-primary, 60%)}; + --aui-color-outline-success: #{mix($color-white, $color-success, 60%)}; + --aui-color-outline-warning: #{mix($color-white, $color-warning, 60%)}; + --aui-color-outline-error: #{mix($color-white, $color-error, 60%)}; + --aui-color-outline-info: #{mix($color-white, $color-info, 60%)}; +} + +@mixin dark-mode { + $color-white: #000; + $color-black: #fff; + + $color-primary: #ff9100; + $color-success: #f540a4; + $color-warning: #0062ff; + $color-danger: #1ababa; + $color-error: #1ababa; + $color-info: #333; + + --aui-color-white: #{$color-white}; + --aui-color-black: #{$color-black}; + --aui-color-primary: #{$color-primary}; + --aui-color-success: #{$color-success}; + --aui-color-warning: #{$color-warning}; + --aui-color-danger: #{$color-danger}; + --aui-color-error: #{$color-error}; + --aui-color-info: #{$color-info}; + --aui-color-primary-light: #{mix($color-white, $color-primary, 60%)}; + --aui-color-success-light: #{mix($color-white, $color-success, 60%)}; + --aui-color-warning-light: #{mix($color-white, $color-warning, 60%)}; + --aui-color-error-light: #{mix($color-white, $color-error, 60%)}; + --aui-color-info-light: #{mix($color-white, $color-info, 60%)}; + --aui-color-primary-lighter: #{mix($color-white, $color-primary, 70%)}; + --aui-color-success-lighter: #{mix($color-white, $color-success, 70%)}; + --aui-color-warning-lighter: #{mix($color-white, $color-warning, 70%)}; + --aui-color-error-lighter: #{mix($color-white, $color-error, 70%)}; + --aui-color-info-lighter: #{mix($color-white, $color-info, 70%)}; + --aui-color-primary-extra-light: #{mix($color-white, $color-primary, 90%)}; + --aui-color-success-extra-light: #{mix($color-white, $color-success, 90%)}; + --aui-color-warning-extra-light: #{mix($color-white, $color-warning, 90%)}; + --aui-color-error-extra-light: #{mix($color-white, $color-error, 90%)}; + --aui-color-info-extra-light: #{mix($color-white, $color-info, 90%)}; + --aui-color-primary-ultra-light: #{mix($color-white, $color-primary, 95%)}; + --aui-color-success-ultra-light: #{mix($color-white, $color-success, 95%)}; + --aui-color-warning-ultra-light: #{mix($color-white, $color-warning, 95%)}; + --aui-color-error-ultra-light: #{mix($color-white, $color-error, 95%)}; + --aui-color-info-ultra-light: #{mix($color-white, $color-info, 95%)}; + --aui-color-primary-dark: #{mix($color-black, $color-primary, 10%)}; + --aui-color-success-dark: #{mix($color-black, $color-success, 10%)}; + --aui-color-warning-dark: #{mix($color-black, $color-warning, 10%)}; + --aui-color-error-dark: #{mix($color-black, $color-error, 10%)}; + --aui-color-info-dark: #{mix($color-black, $color-info, 10%)}; + --aui-color-primary-darker: #{mix($color-black, $color-primary, 20%)}; + --aui-color-success-darker: #{mix($color-black, $color-success, 20%)}; + --aui-color-warning-darker: #{mix($color-black, $color-warning, 20%)}; + --aui-color-error-darker: #{mix($color-black, $color-error, 20%)}; + --aui-color-info-darker: #{mix($color-black, $color-info, 20%)}; + --aui-color-text-main: #ccc; + --aui-color-text-secondary: #999; + --aui-color-text-help: #666; + --aui-color-text-placeholder: #333; + --aui-color-disabled-bg: #0d0d0d; + --aui-color-outline-primary: #{mix($color-white, $color-primary, 60%)}; + --aui-color-outline-success: #{mix($color-white, $color-success, 60%)}; + --aui-color-outline-warning: #{mix($color-white, $color-warning, 60%)}; + --aui-color-outline-error: #{mix($color-white, $color-error, 60%)}; + --aui-color-outline-info: #{mix($color-white, $color-info, 60%)}; +} diff --git a/src/theme/_mixin.scss b/src/theme/_mixin.scss index 7b0e4b631..34ed72908 100644 --- a/src/theme/_mixin.scss +++ b/src/theme/_mixin.scss @@ -47,7 +47,7 @@ } } -@mixin text-overflow() { +@mixin text-overflow { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -58,3 +58,10 @@ align-items: center; justify-content: center; } + +@mixin point($size, $color: #000) { + width: $size; + height: $size; + border-radius: 50%; + background-color: $color; +} diff --git a/src/theme/_var.scss b/src/theme/_var.scss index 3c113b845..c9c62d246 100644 --- a/src/theme/_var.scss +++ b/src/theme/_var.scss @@ -157,8 +157,8 @@ $nav-menu-color: $color-text-help; $nav-menu-color-hover: $color-white; $nav-menu-divider-color: #555; $nav-menu-bg-color-main: #333; -$nav-menu-bg-color-secondary: mix($color-black, $nav-menu-bg-color-main, 24%); -$nav-menu-bg-color-tertiary: mix($color-white, $nav-menu-bg-color-main, 5%); +$nav-menu-bg-color-secondary: mix(#000, $nav-menu-bg-color-main, 24%); +$nav-menu-bg-color-tertiary: mix(#fff, $nav-menu-bg-color-main, 5%); $nav-item-selected-marker-width: 4px; $nav-item-selected-marker-color: $color-primary; diff --git a/src/theme/public-api.ts b/src/theme/public-api.ts new file mode 100644 index 000000000..1b62ce41d --- /dev/null +++ b/src/theme/public-api.ts @@ -0,0 +1 @@ +export * from './theme.service'; diff --git a/src/theme/style.scss b/src/theme/style.scss index 0be8a0a0e..b635808f3 100644 --- a/src/theme/style.scss +++ b/src/theme/style.scss @@ -1,5 +1,24 @@ @import 'base-var'; @import 'mixin'; +@import 'colors'; + +:root { + @include light-mode; +} + +@media (prefers-color-scheme: dark) { + [aui-color-mode='auto'] { + @include dark-mode; + } +} + +html[aui-color-mode='light'] { + @include light-mode; +} + +html[aui-color-mode='dark'] { + @include dark-mode; +} body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', diff --git a/src/theme/theme.service.ts b/src/theme/theme.service.ts new file mode 100644 index 000000000..a0ca32b34 --- /dev/null +++ b/src/theme/theme.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class ThemeService { + getCurrentColorMode(): ColorMode { + return ( + (document + .querySelector('html') + .getAttribute('aui-color-mode') as ColorMode) || 'light' + ); + } + + setColorMode(mode: ColorMode) { + document.querySelector('html').setAttribute('aui-color-mode', mode); + } +} + +export type ColorMode = 'auto' | 'light' | 'dark'; diff --git a/src/tooltip/base-tooltip.ts b/src/tooltip/base-tooltip.ts index b0f08b67e..a81a7246d 100644 --- a/src/tooltip/base-tooltip.ts +++ b/src/tooltip/base-tooltip.ts @@ -15,7 +15,7 @@ import { import { ReplaySubject, merge } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; -import { sleep } from '../utils/async'; +import { sleep } from '../utils'; import { TooltipComponent } from './tooltip.component'; import { TooltipTrigger, TooltipType } from './tooltip.types'; diff --git a/src/tooltip/tooltip.directive.spec.ts b/src/tooltip/tooltip.directive.spec.ts index 93bf49184..695bb9d99 100644 --- a/src/tooltip/tooltip.directive.spec.ts +++ b/src/tooltip/tooltip.directive.spec.ts @@ -139,27 +139,25 @@ describe('TooltipDirective', () => { ); }); - it('should emit show event', () => { - return new Promise(resolve => { + it('should emit show event', () => + new Promise(resolve => { ins.tooltip.show.subscribe(resolve); ins.trigger = TooltipTrigger.Click; fixture.detectChanges(); hostEl.dispatchEvent(new Event('click')); fixture.detectChanges(); - }); - }); + })); - it('should emit hide event', () => { - return new Promise(resolve => { + it('should emit hide event', () => + new Promise(resolve => { ins.tooltip.hide.subscribe(resolve); ins.trigger = TooltipTrigger.Click; fixture.detectChanges(); hostEl.dispatchEvent(new Event('click')); document.body.click(); - }); - }); + })); }); @Component({ diff --git a/src/tree-select/tree-node/tree-node.component.ts b/src/tree-select/tree-node/tree-node.component.ts index c19a9b819..302844f80 100644 --- a/src/tree-select/tree-node/tree-node.component.ts +++ b/src/tree-select/tree-node/tree-node.component.ts @@ -24,8 +24,7 @@ import { tap, } from 'rxjs/operators'; -import { Bem, buildBem } from '../../utils/bem'; -import { scrollIntoView } from '../../utils/scroll-into-view'; +import { Bem, buildBem, scrollIntoView } from '../../utils'; import { TreeSelectComponent } from '../tree-select.component'; import { TreeNode } from '../tree-select.types'; diff --git a/src/tree-select/tree-select.component.ts b/src/tree-select/tree-select.component.ts index 4619fec40..bfa8a9de1 100644 --- a/src/tree-select/tree-select.component.ts +++ b/src/tree-select/tree-select.component.ts @@ -16,7 +16,7 @@ import { CommonFormControl } from '../form/public-api'; import { InputComponent } from '../input/public-api'; import { SelectPrimitiveValue, TrackFn } from '../select/select.types'; import { TooltipDirective } from '../tooltip/public-api'; -import { coerceAttrBoolean, coerceString } from '../utils/coercion'; +import { coerceAttrBoolean, coerceString } from '../utils'; import { TreeNodeComponent } from './tree-node/tree-node.component'; import { TreeNode } from './tree-select.types'; @@ -233,12 +233,11 @@ export class TreeSelectComponent< trackByLabel = (_: number, node: TreeNode) => this.getLabelFromNode(node); private flatNodesData(nodes: Array> = []): Array> { - return nodes.reduce((prevValue: Array>, currentNode) => { - return prevValue.concat( - currentNode, - this.flatNodesData(currentNode.children), - ); - }, []); + return nodes.reduce( + (prevValue: Array>, currentNode) => + prevValue.concat(currentNode, this.flatNodesData(currentNode.children)), + [], + ); } private getLabelFromNode(node: TreeNode) { diff --git a/src/utils/fn.ts b/src/utils/fn.ts index 309e023b7..0ac514fa6 100644 --- a/src/utils/fn.ts +++ b/src/utils/fn.ts @@ -1,3 +1,11 @@ +import { TemplateRef } from '@angular/core'; + export function isUndefined(val: unknown): boolean { return val === undefined; } + +export const last = (values: T[]) => values?.[values.length - 1]; + +export const isTemplateRef = ( + label: string | TemplateRef, +): label is TemplateRef => label instanceof TemplateRef; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 000000000..456adef13 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,7 @@ +export * from './async'; +export * from './bem'; +export * from './coercion'; +export * from './fn'; +export * from './observe-dom'; +export * from './scroll-into-view'; +export * from './watch-content-exist'; diff --git a/src/utils/observe-dom.ts b/src/utils/observe-dom.ts new file mode 100644 index 000000000..69d49024c --- /dev/null +++ b/src/utils/observe-dom.ts @@ -0,0 +1,32 @@ +import { Observable } from 'rxjs'; + +export const observeResizeOn = ( + target: T, + options?: ResizeObserverOptions, +) => + new Observable(observer => { + const resizeObserver = new ResizeObserver(entries => { + for (const entry of entries) { + observer.next(entry.target as T); + } + }); + resizeObserver.observe(target, options); + return () => { + resizeObserver.unobserve(target); + resizeObserver.disconnect(); + }; + }); + +export const observeMutationOn = ( + target: T, + options?: MutationObserverInit, +) => + new Observable(observer => { + const mutationObserver = new MutationObserver(mutations => { + for (const mutation of mutations) { + observer.next(mutation); + } + }); + mutationObserver.observe(target, options); + return () => mutationObserver.disconnect(); + }); diff --git a/stories/.eslintrc b/stories/.eslintrc index d849cd908..7123ae76c 100644 --- a/stories/.eslintrc +++ b/stories/.eslintrc @@ -5,8 +5,5 @@ "@alauda/ui" ] } - }, - "rules": { - "@typescript-eslint/no-extraneous-class": 0 } } diff --git a/stories/anchor/anchor.stories.mdx b/stories/anchor/anchor.stories.mdx new file mode 100644 index 000000000..9e249e5ed --- /dev/null +++ b/stories/anchor/anchor.stories.mdx @@ -0,0 +1,114 @@ +import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks'; +import { moduleMetadata } from '@storybook/angular'; +import { ScrollingModule } from '@angular/cdk/scrolling'; + +import { AnchorModule } from '@alauda/ui'; + + + +# Anchor + + + + {{ + template: /* HTML */ ` +
+
    +
  • + +
    Content 1
    +
      +
    • + +
      SubContent 1-1
      +
    • +
    • + +
      SubContent 1-2
      +
    • +
    +
  • +
  • + +
    Content 2
    +
      +
    • + +
      SubContent 2-1
      +
    • +
    • + +
      SubContent 2-2
      +
    • +
    +
  • +
  • + +
    Content 3
    +
  • +
  • + +
    Content 4
    +
  • +
+
+ `, + }} +
+
+ + + + {{ + template: /* HTML */ ` +
+
    +
  • + +
    Content 1
    +
      +
    • + +
      SubContent 1-1
      +
    • +
    • + +
      SubContent 1-2
      +
    • +
    +
  • +
  • + +
    Content 2
    +
      +
    • + +
      SubContent 2-1
      +
    • +
    • + +
      SubContent 2-2
      +
    • +
    +
  • +
  • + +
    Content 3
    +
  • +
  • + +
    Content 4
    +
  • +
+
+ `, + }} +
+
diff --git a/stories/autocomplete/autocomplete.stories.mdx b/stories/autocomplete/autocomplete.stories.mdx index f1667ba6d..05f94dd9f 100644 --- a/stories/autocomplete/autocomplete.stories.mdx +++ b/stories/autocomplete/autocomplete.stories.mdx @@ -1,4 +1,4 @@ -import { Meta, Preview, Story } from '@storybook/addon-docs/blocks'; +import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks'; import { moduleMetadata } from '@storybook/angular'; import { AutocompleteModule, InputModule } from '@alauda/ui'; @@ -18,7 +18,7 @@ import { AutocompleteModule, InputModule } from '@alauda/ui'; ## 基本自动补全 - + {{ template: /* HTML */ ` @@ -35,11 +35,11 @@ import { AutocompleteModule, InputModule } from '@alauda/ui'; `, }} - + ## 自动补全分组 - + {{ template: /* HTML */ ` @@ -66,11 +66,11 @@ import { AutocompleteModule, InputModule } from '@alauda/ui'; `, }} - + ## 不默认选中第一个 - + {{ template: /* HTML */ ` @@ -92,11 +92,11 @@ import { AutocompleteModule, InputModule } from '@alauda/ui'; `, }} - + ## 点击不显示推荐项 - + {{ template: /* HTML */ ` @@ -119,7 +119,7 @@ import { AutocompleteModule, InputModule } from '@alauda/ui'; `, }} - + ## Autocomplete Inputs diff --git a/stories/back-top/back-top.stories.mdx b/stories/back-top/back-top.stories.mdx index 850842720..4abcc8e15 100644 --- a/stories/back-top/back-top.stories.mdx +++ b/stories/back-top/back-top.stories.mdx @@ -1,4 +1,4 @@ -import { Meta, Preview, Story } from '@storybook/addon-docs/blocks'; +import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks'; import { moduleMetadata } from '@storybook/angular'; import { ScrollingModule } from '@angular/cdk/scrolling'; @@ -19,7 +19,7 @@ import { BackTopModule } from '@alauda/ui'; ## 默认指定外部的 cdkScrollable 组件 - + {{ template: /* HTML */ ` @@ -32,11 +32,11 @@ import { BackTopModule } from '@alauda/ui'; `, }} - + ## 通过模板变量选择滚动容器 - + {{ template: /* HTML */ ` @@ -49,11 +49,11 @@ import { BackTopModule } from '@alauda/ui'; `, }} - + ## 无外部滚动容器,默认根据 window - + {{ template: /* HTML */ ` @@ -64,11 +64,11 @@ import { BackTopModule } from '@alauda/ui'; `, }} - + ## 根据 selector 指定滚动容器 - + {{ template: /* HTML */ ` @@ -81,7 +81,7 @@ import { BackTopModule } from '@alauda/ui'; `, }} - + ## Backtop Inputs diff --git a/stories/breadcrumb/index.ts b/stories/breadcrumb/index.ts index 33281f053..28b0c217f 100644 --- a/stories/breadcrumb/index.ts +++ b/stories/breadcrumb/index.ts @@ -1,27 +1,25 @@ import { BreadcrumbModule } from '@alauda/ui'; import { storiesOf } from '@storybook/angular'; -storiesOf('Breadcrumb', module).add('breadcrumb', () => { - return { - moduleMetadata: { - imports: [BreadcrumbModule], - }, - template: /* HTML */ ` - - storybook - breadcrumb - default - - - storybook - breadcrumb - default - - - storybook - breadcrumb - default - - `, - }; -}); +storiesOf('Breadcrumb', module).add('breadcrumb', () => ({ + moduleMetadata: { + imports: [BreadcrumbModule], + }, + template: /* HTML */ ` + + storybook + breadcrumb + default + + + storybook + breadcrumb + default + + + storybook + breadcrumb + default + + `, +})); diff --git a/stories/button/button.stories.mdx b/stories/button/button.stories.mdx index bf622f578..ceb332b16 100644 --- a/stories/button/button.stories.mdx +++ b/stories/button/button.stories.mdx @@ -1,4 +1,4 @@ -import { Meta, Preview, Story } from '@storybook/addon-docs/blocks'; +import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks'; import { moduleMetadata } from '@storybook/angular'; import { ButtonModule, IconModule } from '@alauda/ui'; @@ -18,7 +18,7 @@ import { ButtonModule, IconModule } from '@alauda/ui'; ## 基本按钮 - + {{ template: ` @@ -31,11 +31,11 @@ import { ButtonModule, IconModule } from '@alauda/ui'; `, }} - + ## 朴素按钮 - + {{ template: ` @@ -48,11 +48,11 @@ import { ButtonModule, IconModule } from '@alauda/ui'; `, }} - + ## 圆角按钮 - + {{ template: ` @@ -65,11 +65,11 @@ import { ButtonModule, IconModule } from '@alauda/ui'; `, }} - + ## 禁用状态 - + {{ template: ` @@ -89,11 +89,11 @@ import { ButtonModule, IconModule } from '@alauda/ui'; `, }} - + ## 文字按钮 - + {{ template: ` @@ -102,11 +102,11 @@ import { ButtonModule, IconModule } from '@alauda/ui'; `, }} - + ## 图标按钮 - + {{ template: ` @@ -127,11 +127,11 @@ import { ButtonModule, IconModule } from '@alauda/ui'; `, }} - + ## 加载中 - + {{ template: ` @@ -142,11 +142,11 @@ import { ButtonModule, IconModule } from '@alauda/ui'; `, }} - + ## 尺寸 - + {{ template: ` @@ -163,11 +163,11 @@ import { ButtonModule, IconModule } from '@alauda/ui'; `, }} - + ## 按钮组 - + {{ template: ` @@ -200,7 +200,7 @@ import { ButtonModule, IconModule } from '@alauda/ui'; `, }} - + ## Inputs diff --git a/stories/card/card.stories.mdx b/stories/card/card.stories.mdx index 6c790ce0c..00a38ce96 100644 --- a/stories/card/card.stories.mdx +++ b/stories/card/card.stories.mdx @@ -1,4 +1,4 @@ -import { Meta, Preview, Story } from '@storybook/addon-docs/blocks'; +import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks'; import { moduleMetadata } from '@storybook/angular'; import { CardModule } from '@alauda/ui'; @@ -18,7 +18,7 @@ import { CardModule } from '@alauda/ui'; ## 基本使用 - + {{ template: /* HTML */ ` @@ -32,11 +32,11 @@ import { CardModule } from '@alauda/ui'; `, }} - + ## 嵌套使用 - + {{ template: /* HTML */ ` @@ -54,11 +54,11 @@ import { CardModule } from '@alauda/ui'; `, }} - + ## 包含 Section - + {{ template: /* HTML */ ` @@ -77,7 +77,7 @@ import { CardModule } from '@alauda/ui'; `, }} - + ## Card Inputs diff --git a/stories/checkbox/checkbox.stories.mdx b/stories/checkbox/checkbox.stories.mdx index 43af87331..cdc605ce1 100644 --- a/stories/checkbox/checkbox.stories.mdx +++ b/stories/checkbox/checkbox.stories.mdx @@ -1,4 +1,4 @@ -import { Meta, Preview, Story } from '@storybook/addon-docs/blocks'; +import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks'; import { moduleMetadata } from '@storybook/angular'; import { IndeterminateCheckboxComponent } from './indeterminate-checkbox/component'; @@ -19,7 +19,7 @@ import { CheckboxModule } from '@alauda/ui'; ## 基本用法 - + {{ template: /* HTML */ ` @@ -36,11 +36,11 @@ import { CheckboxModule } from '@alauda/ui'; }, }} - + ## 多选组合 - + {{ template: /* HTML */ ` @@ -57,11 +57,11 @@ import { CheckboxModule } from '@alauda/ui'; }, }} - + ## 标签样式 - + {{ template: /* HTML */ ` @@ -78,11 +78,11 @@ import { CheckboxModule } from '@alauda/ui'; }, }} - + ## 禁用状态 - + {{ template: /* HTML */ ` @@ -99,19 +99,19 @@ import { CheckboxModule } from '@alauda/ui'; }, }} - + ## 部分选中状态 - + {{ component: IndeterminateCheckboxComponent }} - + ## 垂直布局 - + {{ template: /* HTML */ ` @@ -128,7 +128,7 @@ import { CheckboxModule } from '@alauda/ui'; }, }} - + ## Checkbox Group Inputs diff --git a/stories/dialog/index.ts b/stories/dialog/index.ts index 50f0e4836..256487e9d 100644 --- a/stories/dialog/index.ts +++ b/stories/dialog/index.ts @@ -13,16 +13,14 @@ import { Component, OnDestroy, TemplateRef } from '@angular/core'; import { action } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/angular'; -storiesOf('Dialog', module).add('dialog', () => { - return { - moduleMetadata: { - imports: [ButtonModule, DialogModule, SelectModule], - declarations: [DemoComponent, DialogContentComponent], - entryComponents: [DialogContentComponent], - }, - component: DemoComponent, - }; -}); +storiesOf('Dialog', module).add('dialog', () => ({ + moduleMetadata: { + imports: [ButtonModule, DialogModule, SelectModule], + declarations: [DemoComponent, DialogContentComponent], + entryComponents: [DialogContentComponent], + }, + component: DemoComponent, +})); @Component({ template: ` diff --git a/stories/dropdown/dropdown.stories.mdx b/stories/dropdown/dropdown.stories.mdx index 4cf702ec0..094438719 100644 --- a/stories/dropdown/dropdown.stories.mdx +++ b/stories/dropdown/dropdown.stories.mdx @@ -1,4 +1,4 @@ -import { Meta, Preview, Story } from '@storybook/addon-docs/blocks'; +import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks'; import { moduleMetadata } from '@storybook/angular'; import { ButtonModule, IconModule, DropdownModule } from '@alauda/ui'; @@ -18,7 +18,7 @@ import { ButtonModule, IconModule, DropdownModule } from '@alauda/ui'; ## 基础用法 - + {{ template: /* HTML */ `