mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(menu): add menu trigger support (#867)
* feat(menu): add menu trigger support * addressed comments * lazy create menu
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
describe('menu', function () {
|
||||
beforeEach(function() {
|
||||
browser.get('/menu');
|
||||
});
|
||||
|
||||
it('should open menu when the trigger is clicked', function () {
|
||||
expectMenuPresent(false);
|
||||
element(by.id('trigger')).click();
|
||||
|
||||
expectMenuPresent(true);
|
||||
expect(element(by.css('.md-menu')).getText()).toEqual("One\nTwo\nThree");
|
||||
});
|
||||
|
||||
it('should align menu when open', function() {
|
||||
element(by.id('trigger')).click();
|
||||
expectMenuAlignedWith('trigger');
|
||||
});
|
||||
|
||||
it('should close menu when area outside menu is clicked', function () {
|
||||
element(by.id('trigger')).click();
|
||||
element(by.tagName('body')).click();
|
||||
expectMenuPresent(false);
|
||||
});
|
||||
|
||||
it('should close menu when menu item is clicked', function () {
|
||||
element(by.id('trigger')).click();
|
||||
element(by.id('one')).click();
|
||||
expectMenuPresent(false);
|
||||
});
|
||||
|
||||
it('should run click handlers on regular menu items', function() {
|
||||
element(by.id('trigger')).click();
|
||||
element(by.id('one')).click();
|
||||
expect(element(by.id('text')).getText()).toEqual('one');
|
||||
|
||||
element(by.id('trigger')).click();
|
||||
element(by.id('two')).click();
|
||||
expect(element(by.id('text')).getText()).toEqual('two');
|
||||
});
|
||||
|
||||
it('should run not run click handlers on disabled menu items', function() {
|
||||
element(by.id('trigger')).click();
|
||||
element(by.id('three')).click();
|
||||
expect(element(by.id('text')).getText()).toEqual('');
|
||||
});
|
||||
|
||||
it('should support multiple triggers opening the same menu', function() {
|
||||
element(by.id('trigger-two')).click();
|
||||
expect(element(by.css('.md-menu')).getText()).toEqual("One\nTwo\nThree");
|
||||
expectMenuAlignedWith('trigger-two');
|
||||
|
||||
element(by.tagName('body')).click();
|
||||
expectMenuPresent(false);
|
||||
|
||||
element(by.id('trigger')).click();
|
||||
expect(element(by.css('.md-menu')).getText()).toEqual("One\nTwo\nThree");
|
||||
expectMenuAlignedWith('trigger');
|
||||
|
||||
element(by.tagName('body')).click();
|
||||
expectMenuPresent(false);
|
||||
});
|
||||
|
||||
function expectMenuPresent(bool: boolean) {
|
||||
return browser.isElementPresent(by.css('.md-menu')).then((isPresent) => {
|
||||
expect(isPresent).toBe(bool);
|
||||
});
|
||||
}
|
||||
|
||||
function expectMenuAlignedWith(id: string) {
|
||||
element(by.id(id)).getLocation().then((loc) => {
|
||||
expectMenuLocation({x: loc.x, y: loc.y});
|
||||
});
|
||||
}
|
||||
|
||||
function expectMenuLocation({x,y}: {x: number, y: number}) {
|
||||
element(by.css('.md-menu')).getLocation().then((loc) => {
|
||||
expect(loc.x).toEqual(x);
|
||||
expect(loc.y).toEqual(y);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import {MdError} from '@angular2-material/core/errors/error';
|
||||
|
||||
/**
|
||||
* Exception thrown when menu trigger doesn't have a valid md-menu instance
|
||||
*/
|
||||
export class MdMenuMissingError extends MdError {
|
||||
constructor() {
|
||||
super(`md-menu-trigger: must pass in an md-menu instance.
|
||||
|
||||
Example:
|
||||
<md-menu #menu="mdMenu"></md-menu>
|
||||
<button [md-menu-trigger-for]="menu"></button>
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import {Directive, Input, HostBinding} from '@angular/core';
|
||||
|
||||
/**
|
||||
* This directive is intended to be used inside an md-menu tag.
|
||||
* It exists mostly to set the role attribute.
|
||||
*/
|
||||
@Directive({
|
||||
selector: 'button[md-menu-item]',
|
||||
host: {'role': 'menuitem'}
|
||||
})
|
||||
export class MdMenuItem {}
|
||||
|
||||
/**
|
||||
* This directive is intended to be used inside an md-menu tag.
|
||||
* It sets the role attribute and adds support for the disabled property to anchors.
|
||||
*/
|
||||
@Directive({
|
||||
selector: 'a[md-menu-item]',
|
||||
host: {
|
||||
'role': 'menuitem',
|
||||
'(click)': 'checkDisabled($event)'
|
||||
}
|
||||
})
|
||||
export class MdMenuAnchor {
|
||||
_disabled: boolean;
|
||||
|
||||
@HostBinding('attr.disabled')
|
||||
@Input()
|
||||
get disabled(): boolean {
|
||||
return this._disabled;
|
||||
}
|
||||
|
||||
set disabled(value: boolean) {
|
||||
this._disabled = (value === false || value === undefined) ? null : true;
|
||||
}
|
||||
|
||||
@HostBinding('attr.aria-disabled')
|
||||
get isAriaDisabled(): string {
|
||||
return String(this.disabled);
|
||||
}
|
||||
|
||||
@HostBinding('tabIndex')
|
||||
get tabIndex(): number {
|
||||
return this.disabled ? -1 : 0;
|
||||
}
|
||||
|
||||
checkDisabled(event: Event) {
|
||||
if (this.disabled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import {
|
||||
Directive,
|
||||
ElementRef,
|
||||
Input,
|
||||
Output,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
ViewContainerRef,
|
||||
AfterViewInit,
|
||||
OnDestroy
|
||||
} from '@angular/core';
|
||||
import {MdMenu} from './menu';
|
||||
import {MdMenuItem, MdMenuAnchor} from './menu-item';
|
||||
import {MdMenuMissingError} from './menu-errors';
|
||||
import {
|
||||
Overlay,
|
||||
OverlayState,
|
||||
OverlayRef,
|
||||
OVERLAY_PROVIDERS,
|
||||
TemplatePortal
|
||||
} from '@angular2-material/core/core';
|
||||
import {
|
||||
ConnectedPositionStrategy
|
||||
} from '@angular2-material/core/overlay/position/connected-position-strategy';
|
||||
|
||||
/**
|
||||
* This directive is intended to be used in conjunction with an md-menu tag. It is
|
||||
* responsible for toggling the display of the provided menu instance.
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[md-menu-trigger-for]',
|
||||
host: {'aria-haspopup': 'true'},
|
||||
providers: [OVERLAY_PROVIDERS],
|
||||
exportAs: 'mdMenuTrigger'
|
||||
})
|
||||
export class MdMenuTrigger implements AfterViewInit, OnDestroy {
|
||||
private _portal: TemplatePortal;
|
||||
private _overlayRef: OverlayRef;
|
||||
menuOpen: boolean = false;
|
||||
|
||||
@Input('md-menu-trigger-for') menu: MdMenu;
|
||||
@Output() onMenuOpen = new EventEmitter();
|
||||
@Output() onMenuClose = new EventEmitter();
|
||||
|
||||
constructor(private _overlay: Overlay, private _element: ElementRef,
|
||||
private _viewContainerRef: ViewContainerRef) {}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this._checkMenu();
|
||||
this.menu.close.subscribe(() => this.closeMenu());
|
||||
}
|
||||
|
||||
ngOnDestroy() { this.destroyMenu(); }
|
||||
|
||||
@HostListener('click')
|
||||
toggleMenu(): Promise<void> {
|
||||
return this.menuOpen ? this.closeMenu() : this.openMenu();
|
||||
}
|
||||
|
||||
openMenu(): Promise<void> {
|
||||
return this._createOverlay()
|
||||
.then(() => this._overlayRef.attach(this._portal))
|
||||
.then(() => this._setIsMenuOpen(true));
|
||||
}
|
||||
|
||||
closeMenu(): Promise<void> {
|
||||
if (!this._overlayRef) { return Promise.resolve(); }
|
||||
|
||||
return this._overlayRef.detach()
|
||||
.then(() => this._setIsMenuOpen(false));
|
||||
}
|
||||
|
||||
destroyMenu(): void {
|
||||
this._overlayRef.dispose();
|
||||
}
|
||||
|
||||
// set state rather than toggle to support triggers sharing a menu
|
||||
private _setIsMenuOpen(isOpen: boolean): void {
|
||||
this.menuOpen = isOpen;
|
||||
this.menu._setClickCatcher(isOpen);
|
||||
this.menuOpen ? this.onMenuOpen.emit(null) : this.onMenuClose.emit(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method checks that a valid instance of MdMenu has been passed into
|
||||
* md-menu-trigger-for. If not, an exception is thrown.
|
||||
*/
|
||||
private _checkMenu() {
|
||||
if (!this.menu || !(this.menu instanceof MdMenu)) {
|
||||
throw new MdMenuMissingError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method creates the overlay from the provided menu's template and saves its
|
||||
* OverlayRef so that it can be attached to the DOM when openMenu is called.
|
||||
*/
|
||||
private _createOverlay(): Promise<any> {
|
||||
if (this._overlayRef) { return Promise.resolve(); }
|
||||
|
||||
this._portal = new TemplatePortal(this.menu.templateRef, this._viewContainerRef);
|
||||
return this._overlay.create(this._getOverlayConfig())
|
||||
.then(overlay => this._overlayRef = overlay);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method builds the configuration object needed to create the overlay, the OverlayState.
|
||||
* @returns OverlayState
|
||||
*/
|
||||
private _getOverlayConfig(): OverlayState {
|
||||
const overlayState = new OverlayState();
|
||||
overlayState.positionStrategy = this._getPosition();
|
||||
return overlayState;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method builds the position strategy for the overlay, so the menu is properly connected
|
||||
* to the trigger.
|
||||
* @returns ConnectedPositionStrategy
|
||||
*/
|
||||
private _getPosition(): ConnectedPositionStrategy {
|
||||
return this._overlay.position().connectedTo(
|
||||
this._element,
|
||||
{originX: 'start', originY: 'top'},
|
||||
{overlayX: 'start', overlayY: 'top'}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const MD_MENU_DIRECTIVES = [MdMenu, MdMenuItem, MdMenuTrigger, MdMenuAnchor];
|
||||
@@ -1,4 +1,6 @@
|
||||
<div class="md-menu">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
|
||||
<template>
|
||||
<div class="md-menu" (click)="_emitCloseEvent()">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</template>
|
||||
<div class="md-menu-click-catcher" *ngIf="_showClickCatcher" (click)="_emitCloseEvent()"></div>
|
||||
@@ -1,9 +1,10 @@
|
||||
// TODO(kara): update vars for desktop when MD team responds
|
||||
|
||||
// TODO(kara): animation for menu opening
|
||||
@import 'variables';
|
||||
@import 'elevation';
|
||||
@import 'default-theme';
|
||||
@import 'button-mixins';
|
||||
@import 'sidenav-mixins';
|
||||
@import 'list-shared';
|
||||
|
||||
// menu width must be a multiple of 56px
|
||||
@@ -24,14 +25,16 @@ $md-menu-side-padding: 16px !default;
|
||||
overflow: scroll;
|
||||
-webkit-overflow-scrolling: touch; // for momentum scroll on mobile
|
||||
|
||||
background: md-color($md-background, 'background');
|
||||
background: md-color($md-background, 'card');
|
||||
}
|
||||
|
||||
[md-menu-item] {
|
||||
@include md-button-reset();
|
||||
@include md-truncate-line();
|
||||
|
||||
display: block;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: $md-menu-item-height;
|
||||
padding: 0 $md-menu-side-padding;
|
||||
@@ -39,6 +42,7 @@ $md-menu-side-padding: 16px !default;
|
||||
font-size: $md-menu-font-size;
|
||||
font-family: $md-font-family;
|
||||
text-align: start;
|
||||
text-decoration: none; // necessary to reset anchor tags
|
||||
color: md-color($md-foreground, 'text');
|
||||
|
||||
&[disabled] {
|
||||
@@ -51,3 +55,6 @@ $md-menu-side-padding: 16px !default;
|
||||
}
|
||||
}
|
||||
|
||||
.md-menu-click-catcher {
|
||||
@include md-fullscreen();
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import {inject} from '@angular/core/testing';
|
||||
import {TestComponentBuilder} from '@angular/compiler/testing';
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
import {MD_MENU_DIRECTIVES} from './menu';
|
||||
import {MD_MENU_DIRECTIVES} from './menu-trigger';
|
||||
|
||||
describe('MdMenu', () => {
|
||||
let builder: TestComponentBuilder;
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
import {Component, Directive, ViewEncapsulation} from '@angular/core';
|
||||
// TODO(kara): keyboard events for menu navigation
|
||||
// TODO(kara): prevent-close functionality
|
||||
// TODO(kara): set position of menu
|
||||
|
||||
import {
|
||||
Component,
|
||||
ViewEncapsulation,
|
||||
Output,
|
||||
ViewChild,
|
||||
TemplateRef,
|
||||
EventEmitter
|
||||
} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
@@ -9,13 +20,23 @@ import {Component, Directive, ViewEncapsulation} from '@angular/core';
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
exportAs: 'mdMenu'
|
||||
})
|
||||
export class MdMenu {}
|
||||
export class MdMenu {
|
||||
private _showClickCatcher: boolean = false;
|
||||
|
||||
@Directive({
|
||||
selector: '[md-menu-item]',
|
||||
host: {'role': 'menuitem'}
|
||||
})
|
||||
export class MdMenuItem {}
|
||||
@Output() close = new EventEmitter;
|
||||
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
|
||||
|
||||
export const MD_MENU_DIRECTIVES = [MdMenu, MdMenuItem];
|
||||
/**
|
||||
* This function toggles the display of the menu's click catcher element.
|
||||
* This element covers the viewport when the menu is open to detect clicks outside the menu.
|
||||
* TODO: internal
|
||||
*/
|
||||
_setClickCatcher(bool: boolean): void {
|
||||
this._showClickCatcher = bool;
|
||||
}
|
||||
|
||||
private _emitCloseEvent(): void {
|
||||
this.close.emit(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@import 'mixins';
|
||||
@import 'variables';
|
||||
@import 'elevation';
|
||||
|
||||
@import 'sidenav-mixins';
|
||||
|
||||
// We use invert() here to have the darken the background color expected to be used. If the
|
||||
// background is light, we use a dark backdrop. If the background is dark, we use a light backdrop.
|
||||
@@ -42,16 +42,6 @@ $md-sidenav-push-background-color: md-color($md-background, dialog) !default;
|
||||
}
|
||||
}
|
||||
|
||||
/* This mixin ensures a sidenav element spans the whole viewport.*/
|
||||
@mixin md-sidenav-fullscreen {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
:host {
|
||||
// We need a stacking context here so that the backdrop and drawers are clipped to the
|
||||
// MdSidenavLayout. This creates a new z-index stack so we use low numbered z-indices.
|
||||
@@ -64,7 +54,7 @@ $md-sidenav-push-background-color: md-color($md-background, dialog) !default;
|
||||
|
||||
// TODO(hansl): Update this with a more robust solution.
|
||||
&[fullscreen] {
|
||||
@include md-sidenav-fullscreen();
|
||||
@include md-fullscreen();
|
||||
|
||||
&.md-sidenav-opened {
|
||||
overflow: hidden;
|
||||
@@ -78,7 +68,7 @@ $md-sidenav-push-background-color: md-color($md-background, dialog) !default;
|
||||
overflow: hidden;
|
||||
|
||||
& > .md-sidenav-backdrop {
|
||||
@include md-sidenav-fullscreen();
|
||||
@include md-fullscreen();
|
||||
|
||||
display: block;
|
||||
|
||||
|
||||
@@ -195,7 +195,8 @@ export abstract class BasePortalHost implements PortalHost {
|
||||
abstract attachTemplatePortal(portal: TemplatePortal): Promise<Map<string, any>>;
|
||||
|
||||
detach(): Promise<void> {
|
||||
this._attachedPortal.setAttachedHost(null);
|
||||
if (this._attachedPortal) { this._attachedPortal.setAttachedHost(null); }
|
||||
|
||||
this._attachedPortal = null;
|
||||
if (this._disposeFn != null) {
|
||||
this._disposeFn();
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/* This mixin ensures an element spans the whole viewport.*/
|
||||
@mixin md-fullscreen {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
@@ -1,5 +1,31 @@
|
||||
<md-menu>
|
||||
<button md-menu-item>Refresh</button>
|
||||
<button md-menu-item>Settings</button>
|
||||
<button md-menu-item disabled>Help</button>
|
||||
</md-menu>
|
||||
<div class="demo-menu">
|
||||
<div class="menu-section">
|
||||
<p>You clicked on: {{ selected }}</p>
|
||||
|
||||
<md-toolbar>
|
||||
<button md-icon-button [md-menu-trigger-for]="menu">
|
||||
<md-icon>more_vert</md-icon>
|
||||
</button>
|
||||
</md-toolbar>
|
||||
|
||||
<md-menu #menu="mdMenu">
|
||||
<button md-menu-item *ngFor="let item of items" (click)="select(item.text)" [disabled]="item.disabled">
|
||||
{{ item.text }}
|
||||
</button>
|
||||
</md-menu>
|
||||
</div>
|
||||
<div class="menu-section">
|
||||
<p> Clicking these will navigate:</p>
|
||||
<md-toolbar>
|
||||
<button md-icon-button [md-menu-trigger-for]="anchorMenu">
|
||||
<md-icon>more_vert</md-icon>
|
||||
</button>
|
||||
</md-toolbar>
|
||||
|
||||
<md-menu #anchorMenu="mdMenu">
|
||||
<a md-menu-item *ngFor="let item of items" href="http://www.google.com" [disabled]="item.disabled">
|
||||
{{ item.text }}
|
||||
</a>
|
||||
</md-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
.demo-menu {
|
||||
display: flex;
|
||||
|
||||
.menu-section {
|
||||
width: 300px;
|
||||
margin: 20px;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,29 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {MD_MENU_DIRECTIVES} from '@angular2-material/menu/menu';
|
||||
import {MD_MENU_DIRECTIVES} from '@angular2-material/menu/menu-trigger';
|
||||
import {MD_ICON_DIRECTIVES} from '@angular2-material/icon/icon';
|
||||
import {MD_BUTTON_DIRECTIVES} from '@angular2-material/button/button';
|
||||
import {MD_TOOLBAR_DIRECTIVES} from '@angular2-material/toolbar/toolbar';
|
||||
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
selector: 'menu-demo',
|
||||
templateUrl: 'menu-demo.html',
|
||||
styleUrls: ['menu-demo.css'],
|
||||
directives: [MD_MENU_DIRECTIVES]
|
||||
directives: [
|
||||
MD_MENU_DIRECTIVES,
|
||||
MD_ICON_DIRECTIVES,
|
||||
MD_BUTTON_DIRECTIVES,
|
||||
MD_TOOLBAR_DIRECTIVES
|
||||
]
|
||||
})
|
||||
export class MenuDemo {}
|
||||
export class MenuDemo {
|
||||
selected = '';
|
||||
items = [
|
||||
{text: 'Refresh'},
|
||||
{text: 'Settings'},
|
||||
{text: 'Help'},
|
||||
{text: 'Sign Out', disabled: true}
|
||||
];
|
||||
|
||||
select(text: string) { this.selected = text; }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<a md-list-item [routerLink]="['button']">Button</a>
|
||||
<a md-list-item [routerLink]="['tabs']">Tabs</a>
|
||||
<a md-list-item [routerLink]="['icon']">Icon</a>
|
||||
<a md-list-item [routerLink]="['menu']">Menu</a>
|
||||
<a md-list-item [routerLink]="['tabs']">Tabs</a>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
@@ -3,12 +3,14 @@ import {Home} from './e2e-app';
|
||||
import {ButtonE2E} from '../button/button-e2e';
|
||||
import {BasicTabs} from '../tabs/tabs-e2e';
|
||||
import {IconE2E} from '../icon/icon-e2e';
|
||||
import {MenuE2E} from '../menu/menu-e2e';
|
||||
|
||||
export const routes: RouterConfig = [
|
||||
{path: '', component: Home},
|
||||
{path: 'button', component: ButtonE2E},
|
||||
{path: 'tabs', component: BasicTabs},
|
||||
{path: 'icon', component: IconE2E}
|
||||
{path: 'menu', component: MenuE2E},
|
||||
{path: 'icon', component: IconE2E},
|
||||
{path: 'tabs', component: BasicTabs}
|
||||
];
|
||||
|
||||
export const E2E_APP_ROUTE_PROVIDER = provideRouter(routes);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<div>
|
||||
<div style="float:left">
|
||||
<div id="text">{{ selected }}</div>
|
||||
<button [md-menu-trigger-for]="menu" id="trigger">TRIGGER</button>
|
||||
<button [md-menu-trigger-for]="menu" id="trigger-two">TRIGGER 2</button>
|
||||
|
||||
<md-menu #menu="mdMenu">
|
||||
<button md-menu-item id="one" (click)="selected='one'">One</button>
|
||||
<button md-menu-item id="two" (click)="selected='two'">Two</button>
|
||||
<button md-menu-item id="three" (click)="selected='three'" disabled>Three</button>
|
||||
</md-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {MD_MENU_DIRECTIVES} from '@angular2-material/menu/menu-trigger';
|
||||
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
selector: 'menu-e2e',
|
||||
templateUrl: 'menu-e2e.html',
|
||||
directives: [MD_MENU_DIRECTIVES]
|
||||
})
|
||||
export class MenuE2E {
|
||||
selected: string = '';
|
||||
}
|
||||
@@ -13,6 +13,7 @@ const components = [
|
||||
'icon',
|
||||
'input',
|
||||
'list',
|
||||
'menu',
|
||||
'progress-bar',
|
||||
'progress-circle',
|
||||
'radio',
|
||||
|
||||
Reference in New Issue
Block a user