) => {
- const actions: MatChipAction[] = [];
- chips.forEach(chip => chip._getActions().forEach(action => actions.push(action)));
- this._chipActions.reset(actions);
- this._chipActions.notifyOnChanges();
- });
-
- this._keyManager = new FocusKeyManager(this._chipActions)
- .withVerticalOrientation()
- .withHorizontalOrientation(this._dir ? this._dir.value : 'ltr')
- .withHomeAndEnd()
- // Skip non-interactive and disabled actions since the user can't do anything with them.
- .skipPredicate(action => !action.isInteractive || action.disabled);
- // Keep the manager active index in sync so that navigation picks
- // up from the current chip if the user clicks into the list directly.
- this.chipFocusChanges.pipe(takeUntil(this._destroyed)).subscribe(({chip}) => {
- const action = chip._getSourceAction(document.activeElement as Element);
-
- if (action) {
- this._keyManager.updateActiveItem(action);
- }
- });
-
- this._dir?.change
- .pipe(takeUntil(this._destroyed))
- .subscribe(direction => this._keyManager.withHorizontalOrientation(direction));
- }
-
- /** Listens to changes in the chip set and syncs up the state of the individual chips. */
- private _trackChipSetChanges() {
- this._chips.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => {
- if (this.disabled) {
- // Since this happens after the content has been
- // checked, we need to defer it to the next tick.
- Promise.resolve().then(() => this._syncChipsState());
- }
-
- this._redirectDestroyedChipFocus();
- });
+ return false;
}
- /** Starts tracking the destroyed chips in order to capture the focused one. */
- private _trackDestroyedFocusedChip() {
- this.chipDestroyedChanges.pipe(takeUntil(this._destroyed)).subscribe((event: MatChipEvent) => {
- const chipArray = this._chips.toArray();
- const chipIndex = chipArray.indexOf(event.chip);
-
- // If the focused chip is destroyed, save its index so that we can move focus to the next
- // chip. We only save the index here, rather than move the focus immediately, because we want
- // to wait until the chip is removed from the chip list before focusing the next one. This
- // allows us to keep focus on the same index if the chip gets swapped out.
- if (this._isValidIndex(chipIndex) && event.chip._hasFocus()) {
- this._lastDestroyedFocusedChipIndex = chipIndex;
- }
- });
+ private _chipFoundation(index: number): MDCChipFoundation | undefined {
+ return this._chips.toArray()[index]?._getFoundation();
}
- /**
- * Finds the next appropriate chip to move focus to,
- * if the currently-focused chip is destroyed.
- */
- private _redirectDestroyedChipFocus() {
- if (this._lastDestroyedFocusedChipIndex == null) {
- return;
- }
+ private _handleChipAnimation = (event: Event) => {
+ this._chipSetFoundation.handleChipAnimation(event as ChipAnimationEvent);
+ };
- if (this._chips.length) {
- const newIndex = Math.min(this._lastDestroyedFocusedChipIndex, this._chips.length - 1);
- const chipToFocus = this._chips.toArray()[newIndex];
-
- if (chipToFocus.disabled) {
- // If we're down to one disabled chip, move focus back to the set.
- if (this._chips.length === 1) {
- this.focus();
- } else {
- this._keyManager.setPreviousItemActive();
- }
- } else {
- chipToFocus.focus();
- }
- } else {
- this.focus();
- }
+ private _handleChipInteraction = (event: Event) => {
+ this._chipSetFoundation.handleChipInteraction(event as ChipInteractionEvent);
+ };
- this._lastDestroyedFocusedChipIndex = null;
- }
+ private _handleChipNavigation = (event: Event) => {
+ this._chipSetFoundation.handleChipNavigation(event as ChipNavigationEvent);
+ };
}
diff --git a/src/material-experimental/mdc-chips/chip.spec.ts b/src/material-experimental/mdc-chips/chip.spec.ts
index a68e7811615c..026bbabc697b 100644
--- a/src/material-experimental/mdc-chips/chip.spec.ts
+++ b/src/material-experimental/mdc-chips/chip.spec.ts
@@ -194,7 +194,7 @@ describe('MDC-based MatChip', () => {
{{name}}
@@ -211,6 +211,7 @@ class SingleChip {
value: any;
rippleDisabled: boolean = false;
+ chipFocus: (event?: MatChipEvent) => void = () => {};
chipDestroy: (event?: MatChipEvent) => void = () => {};
chipRemove: (event?: MatChipEvent) => void = () => {};
}
diff --git a/src/material-experimental/mdc-chips/chip.ts b/src/material-experimental/mdc-chips/chip.ts
index 465473b20570..87af59368d6c 100644
--- a/src/material-experimental/mdc-chips/chip.ts
+++ b/src/material-experimental/mdc-chips/chip.ts
@@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
+import {Directionality} from '@angular/cdk/bidi';
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
import {
@@ -37,16 +38,31 @@ import {
mixinColor,
mixinDisableRipple,
mixinTabIndex,
- mixinDisabled,
RippleGlobalOptions,
} from '@angular/material-experimental/mdc-core';
+import {
+ MDCChipFoundation,
+ MDCChipAdapter,
+ MDCChipActionType,
+ MDCChipActionFocusBehavior,
+ MDCChipActionFoundation,
+ MDCChipActionEvents,
+ ActionInteractionEvent,
+ ActionNavigationEvent,
+ MDCChipActionInteractionTrigger,
+} from '@material/chips';
import {FocusMonitor} from '@angular/cdk/a11y';
import {Subject} from 'rxjs';
-import {take} from 'rxjs/operators';
-import {MatChipAvatar, MatChipTrailingIcon, MatChipRemove} from './chip-icons';
+import {
+ MatChipAvatar,
+ MatChipTrailingIcon,
+ MatChipRemove,
+ MAT_CHIP_AVATAR,
+ MAT_CHIP_TRAILING_ICON,
+ MAT_CHIP_REMOVE,
+} from './chip-icons';
+import {emitCustomEvent} from './emit-event';
import {MatChipAction} from './chip-action';
-import {BACKSPACE, DELETE} from '@angular/cdk/keycodes';
-import {MAT_CHIP, MAT_CHIP_AVATAR, MAT_CHIP_REMOVE, MAT_CHIP_TRAILING_ICON} from './tokens';
let uid = 0;
@@ -60,19 +76,12 @@ export interface MatChipEvent {
* Boilerplate for applying mixins to MatChip.
* @docs-private
*/
-const _MatChipMixinBase = mixinTabIndex(
- mixinColor(
- mixinDisableRipple(
- mixinDisabled(
- class {
- constructor(public _elementRef: ElementRef) {}
- },
- ),
- ),
- 'primary',
- ),
- -1,
-);
+abstract class MatChipBase {
+ abstract disabled: boolean;
+ constructor(public _elementRef: ElementRef) {}
+}
+
+const _MatChipMixinBase = mixinTabIndex(mixinColor(mixinDisableRipple(MatChipBase), 'primary'), -1);
/**
* Material design styled Chip base component. Used inside the MatChipSet component.
@@ -81,7 +90,7 @@ const _MatChipMixinBase = mixinTabIndex(
*/
@Component({
selector: 'mat-basic-chip, mat-chip',
- inputs: ['color', 'disabled', 'disableRipple', 'tabIndex'],
+ inputs: ['color', 'disableRipple', 'tabIndex'],
exportAs: 'matChip',
templateUrl: 'chip.html',
styleUrls: ['chip.css'],
@@ -104,11 +113,9 @@ const _MatChipMixinBase = mixinTabIndex(
'[attr.role]': 'role',
'[attr.tabindex]': 'role ? tabIndex : null',
'[attr.aria-label]': 'ariaLabel',
- '(keydown)': '_handleKeydown($event)',
},
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
- providers: [{provide: MAT_CHIP, useExisting: MatChip}],
})
export class MatChip
extends _MatChipMixinBase
@@ -132,7 +139,7 @@ export class MatChip
@Input() role: string | null = null;
/** Whether the chip has focus. */
- private _hasFocusInternal = false;
+ protected _hasFocusInternal = false;
/** Whether moving focus into the chip is pending. */
private _pendingFocus: boolean;
@@ -150,6 +157,21 @@ export class MatChip
/** ARIA label for the content of the chip. */
@Input('aria-label') ariaLabel: string | null = null;
+ @Input()
+ get disabled(): boolean {
+ return this._disabled;
+ }
+ set disabled(value: BooleanInput) {
+ this._disabled = coerceBooleanProperty(value);
+
+ if (this.removeIcon) {
+ this.removeIcon.disabled = this._disabled;
+ }
+
+ this._chipFoundation.setDisabled(this._disabled);
+ }
+ protected _disabled: boolean = false;
+
private _textElement!: HTMLElement;
/**
@@ -195,6 +217,9 @@ export class MatChip
/** Emitted when the chip is destroyed. */
@Output() readonly destroyed: EventEmitter = new EventEmitter();
+ /** The MDC foundation containing business logic for MDC chip. */
+ _chipFoundation: MDCChipFoundation;
+
/** The unstyled chip selector for this component. */
protected basicChipAttrName = 'mat-basic-chip';
@@ -213,12 +238,67 @@ export class MatChip
/** Action receiving the primary set of user interactions. */
@ViewChild(MatChipAction) primaryAction: MatChipAction;
+ /**
+ * Implementation of the MDC chip adapter interface.
+ * These methods are called by the chip foundation.
+ */
+ protected _chipAdapter: MDCChipAdapter = {
+ addClass: className => this._setMdcClass(className, true),
+ removeClass: className => this._setMdcClass(className, false),
+ hasClass: className => this._elementRef.nativeElement.classList.contains(className),
+ emitEvent: (eventName: string, data: T) => {
+ emitCustomEvent(this._elementRef.nativeElement, this._document, eventName, data, true);
+ },
+ setStyleProperty: (propertyName: string, value: string) => {
+ this._elementRef.nativeElement.style.setProperty(propertyName, value);
+ },
+ isRTL: () => this._dir?.value === 'rtl',
+ getAttribute: attributeName => this._elementRef.nativeElement.getAttribute(attributeName),
+ getElementID: () => this._elementRef.nativeElement.id,
+ getOffsetWidth: () => this._elementRef.nativeElement.offsetWidth,
+ getActions: () => {
+ const result: MDCChipActionType[] = [];
+
+ if (this._getAction(MDCChipActionType.PRIMARY)) {
+ result.push(MDCChipActionType.PRIMARY);
+ }
+
+ if (this._getAction(MDCChipActionType.TRAILING)) {
+ result.push(MDCChipActionType.TRAILING);
+ }
+
+ return result;
+ },
+ isActionSelectable: (action: MDCChipActionType) => {
+ return this._getAction(action)?.isSelectable() || false;
+ },
+ isActionSelected: (action: MDCChipActionType) => {
+ return this._getAction(action)?.isSelected() || false;
+ },
+ isActionDisabled: (action: MDCChipActionType) => {
+ return this._getAction(action)?.isDisabled() || false;
+ },
+ isActionFocusable: (action: MDCChipActionType) => {
+ return this._getAction(action)?.isFocusable() || false;
+ },
+ setActionSelected: (action: MDCChipActionType, isSelected: boolean) => {
+ this._getAction(action)?.setSelected(isSelected);
+ },
+ setActionDisabled: (action: MDCChipActionType, isDisabled: boolean) => {
+ this._getAction(action)?.setDisabled(isDisabled);
+ },
+ setActionFocus: (action: MDCChipActionType, behavior: MDCChipActionFocusBehavior) => {
+ this._getAction(action)?.setFocus(behavior);
+ },
+ };
+
constructor(
public _changeDetectorRef: ChangeDetectorRef,
elementRef: ElementRef,
protected _ngZone: NgZone,
private _focusMonitor: FocusMonitor,
@Inject(DOCUMENT) _document: any,
+ @Optional() private _dir: Directionality,
@Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string,
@Optional()
@Inject(MAT_RIPPLE_GLOBAL_OPTIONS)
@@ -228,18 +308,29 @@ export class MatChip
super(elementRef);
const element = elementRef.nativeElement;
this._document = _document;
+ this._chipFoundation = new MDCChipFoundation(this._chipAdapter);
this._animationsDisabled = animationMode === 'NoopAnimations';
this._isBasicChip =
element.hasAttribute(this.basicChipAttrName) ||
element.tagName.toLowerCase() === this.basicChipAttrName;
+ element.addEventListener(MDCChipActionEvents.INTERACTION, this._handleActionInteraction);
+ element.addEventListener(MDCChipActionEvents.NAVIGATION, this._handleActionNavigation);
+ _focusMonitor.monitor(elementRef, true);
+
+ _ngZone.runOutsideAngular(() => {
+ element.addEventListener('transitionend', this._handleTransitionend);
+ element.addEventListener('animationend', this._handleAnimationend);
+ });
+
if (tabIndex != null) {
this.tabIndex = parseInt(tabIndex) ?? this.defaultTabIndex;
}
- this._monitorFocus();
}
ngAfterViewInit() {
- this._textElement = this._elementRef.nativeElement.querySelector('.mat-mdc-chip-action-label')!;
+ this._chipFoundation.init();
+ this._chipFoundation.setDisabled(this.disabled);
+ this._textElement = this._elementRef.nativeElement.querySelector('.mat-mdc-chip-action-label');
if (this._pendingFocus) {
this._pendingFocus = false;
@@ -248,9 +339,14 @@ export class MatChip
}
ngOnDestroy() {
+ const element = this._elementRef.nativeElement;
+ element.removeEventListener(MDCChipActionEvents.INTERACTION, this._handleActionInteraction);
+ element.removeEventListener(MDCChipActionEvents.NAVIGATION, this._handleActionNavigation);
+ element.removeEventListener('transitionend', this._handleTransitionend);
+ element.removeEventListener('animationend', this._handleAnimationend);
+ this._chipFoundation.destroy();
this._focusMonitor.stopMonitoring(this._elementRef);
this.destroyed.emit({chip: this});
- this.destroyed.complete();
}
/**
@@ -264,6 +360,13 @@ export class MatChip
}
}
+ /** Sets whether the given CSS class should be applied to the MDC chip. */
+ private _setMdcClass(cssClass: string, active: boolean) {
+ const classes = this._elementRef.nativeElement.classList;
+ active ? classes.add(cssClass) : classes.remove(cssClass);
+ this._changeDetectorRef.markForCheck();
+ }
+
/** Whether or not the ripple should be disabled. */
_isRippleDisabled(): boolean {
return (
@@ -275,85 +378,87 @@ export class MatChip
);
}
- /** Returns whether the chip has a trailing icon. */
- _hasTrailingIcon() {
- return !!(this.trailingIcon || this.removeIcon);
- }
-
- /** Handles keyboard events on the chip. */
- _handleKeydown(event: KeyboardEvent) {
- if (event.keyCode === BACKSPACE || event.keyCode === DELETE) {
- event.preventDefault();
- this.remove();
+ _getAction(type: MDCChipActionType): MDCChipActionFoundation | undefined {
+ switch (type) {
+ case MDCChipActionType.PRIMARY:
+ return this.primaryAction?._getFoundation();
+ case MDCChipActionType.TRAILING:
+ return (this.removeIcon || this.trailingIcon)?._getFoundation();
}
- }
- /** Allows for programmatic focusing of the chip. */
- focus(): void {
- if (!this.disabled) {
- // If `focus` is called before `ngAfterViewInit`, we won't have access to the primary action.
- // This can happen if the consumer tries to focus a chip immediately after it is added.
- // Queue the method to be called again on init.
- if (this.primaryAction) {
- this.primaryAction.focus();
- } else {
- this._pendingFocus = true;
- }
- }
+ return undefined;
}
- /** Gets the action that contains a specific target node. */
- _getSourceAction(target: Node): MatChipAction | undefined {
- return this._getActions().find(action => {
- const element = action._elementRef.nativeElement;
- return element === target || element.contains(target);
- });
+ _getFoundation() {
+ return this._chipFoundation;
}
- /** Gets all of the actions within the chip. */
- _getActions(): MatChipAction[] {
- const result: MatChipAction[] = [];
+ _hasTrailingIcon() {
+ return !!(this.trailingIcon || this.removeIcon);
+ }
- if (this.primaryAction) {
- result.push(this.primaryAction);
+ /** Allows for programmatic focusing of the chip. */
+ focus(): void {
+ if (this.disabled) {
+ return;
}
- if (this.removeIcon) {
- result.push(this.removeIcon);
+ // If `focus` is called before `ngAfterViewInit`, we won't have access to the primary action.
+ // This can happen if the consumer tries to focus a chip immediately after it is added.
+ // Queue the method to be called again on init.
+ if (!this.primaryAction) {
+ this._pendingFocus = true;
+ return;
}
- if (this.trailingIcon) {
- result.push(this.trailingIcon);
+ if (!this._hasFocus()) {
+ this._onFocus.next({chip: this});
+ this._hasFocusInternal = true;
}
- return result;
+ this.primaryAction.focus();
}
- /** Handles interactions with the primary action of the chip. */
- _handlePrimaryActionInteraction() {
- // Empty here, but is overwritten in child classes.
+ /** Overridden by MatChipOption. */
+ protected _onChipInteraction(event: ActionInteractionEvent) {
+ const removeElement = this.removeIcon?._elementRef.nativeElement;
+ const trigger = event.detail.trigger;
+
+ // MDC's removal process requires an `animationend` event followed by a `transitionend`
+ // event coming from the chip, which in turn will call `remove`. While we can stub
+ // out these events in our own tests, they can be difficult to fake for consumers that are
+ // testing our components or are wrapping them. We skip the entire sequence and trigger the
+ // removal directly in order to make the component easier to deal with.
+ if (
+ removeElement &&
+ (trigger === MDCChipActionInteractionTrigger.CLICK ||
+ trigger === MDCChipActionInteractionTrigger.ENTER_KEY ||
+ trigger === MDCChipActionInteractionTrigger.SPACEBAR_KEY) &&
+ (event.target === removeElement || removeElement.contains(event.target))
+ ) {
+ this.remove();
+ } else {
+ this._chipFoundation.handleActionInteraction(event);
+ }
}
- /** Starts the focus monitoring process on the chip. */
- private _monitorFocus() {
- this._focusMonitor.monitor(this._elementRef, true).subscribe(origin => {
- const hasFocus = origin !== null;
-
- if (hasFocus !== this._hasFocusInternal) {
- this._hasFocusInternal = hasFocus;
-
- if (hasFocus) {
- this._onFocus.next({chip: this});
- } else {
- // When animations are enabled, Angular may end up removing the chip from the DOM a little
- // earlier than usual, causing it to be blurred and throwing off the logic in the chip list
- // that moves focus not the next item. To work around the issue, we defer marking the chip
- // as not focused until the next time the zone stabilizes.
- this._ngZone.onStable
- .pipe(take(1))
- .subscribe(() => this._ngZone.run(() => this._onBlur.next({chip: this})));
- }
- }
- });
- }
+ private _handleActionInteraction = (event: Event) => {
+ this._onChipInteraction(event as ActionInteractionEvent);
+ };
+
+ private _handleActionNavigation = (event: Event) => {
+ this._chipFoundation.handleActionNavigation(event as ActionNavigationEvent);
+ };
+
+ private _handleTransitionend = (event: TransitionEvent) => {
+ if (event.target === this._elementRef.nativeElement) {
+ this._ngZone.run(() => this._chipFoundation.handleTransitionEnd());
+ }
+ };
+
+ private _handleAnimationend = (event: AnimationEvent) => {
+ if (event.target === this._elementRef.nativeElement) {
+ this._ngZone.run(() => this._chipFoundation.handleAnimationEnd(event));
+ }
+ };
}
diff --git a/src/material-experimental/mdc-chips/emit-event.ts b/src/material-experimental/mdc-chips/emit-event.ts
new file mode 100644
index 000000000000..b208b6d3f0d8
--- /dev/null
+++ b/src/material-experimental/mdc-chips/emit-event.ts
@@ -0,0 +1,33 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+/**
+ * Emits a custom event from an element.
+ * @param element Element from which to emit the event.
+ * @param _document Document that the element is placed in.
+ * @param eventName Name of the event.
+ * @param data Data attached to the event.
+ * @param shouldBubble Whether the event should bubble.
+ */
+export function emitCustomEvent(
+ element: HTMLElement,
+ _document: Document,
+ eventName: string,
+ data: T,
+ shouldBubble: boolean,
+): void {
+ let event: CustomEvent;
+ if (typeof CustomEvent === 'function') {
+ event = new CustomEvent(eventName, {bubbles: shouldBubble, detail: data});
+ } else {
+ event = _document.createEvent('CustomEvent');
+ event.initCustomEvent(eventName, shouldBubble, false, data);
+ }
+
+ element.dispatchEvent(event);
+}
diff --git a/src/material-experimental/mdc-chips/module.ts b/src/material-experimental/mdc-chips/module.ts
index 90b564c19a41..ace98686a98e 100644
--- a/src/material-experimental/mdc-chips/module.ts
+++ b/src/material-experimental/mdc-chips/module.ts
@@ -15,7 +15,7 @@ import {
MatRippleModule,
} from '@angular/material-experimental/mdc-core';
import {MatChip} from './chip';
-import {MAT_CHIPS_DEFAULT_OPTIONS, MatChipsDefaultOptions} from './tokens';
+import {MAT_CHIPS_DEFAULT_OPTIONS, MatChipsDefaultOptions} from './chip-default-options';
import {MatChipEditInput} from './chip-edit-input';
import {MatChipGrid} from './chip-grid';
import {MatChipAvatar, MatChipRemove, MatChipTrailingIcon} from './chip-icons';
diff --git a/src/material-experimental/mdc-chips/public-api.ts b/src/material-experimental/mdc-chips/public-api.ts
index 4971595984dc..dc6a425f3914 100644
--- a/src/material-experimental/mdc-chips/public-api.ts
+++ b/src/material-experimental/mdc-chips/public-api.ts
@@ -14,7 +14,7 @@ export * from './chip-listbox';
export * from './chip-grid';
export * from './module';
export * from './chip-input';
-export * from './tokens';
+export * from './chip-default-options';
export * from './chip-icons';
export * from './chip-text-control';
export * from './chip-edit-input';
diff --git a/src/material-experimental/mdc-chips/tokens.ts b/src/material-experimental/mdc-chips/tokens.ts
deleted file mode 100644
index ea5cd59883e6..000000000000
--- a/src/material-experimental/mdc-chips/tokens.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * @license
- * Copyright Google LLC All Rights Reserved.
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://angular.io/license
- */
-
-import {InjectionToken} from '@angular/core';
-
-/** Default options, for the chips module, that can be overridden. */
-export interface MatChipsDefaultOptions {
- /** The list of key codes that will trigger a chipEnd event. */
- separatorKeyCodes: readonly number[] | ReadonlySet;
-}
-
-/** Injection token to be used to override the default options for the chips module. */
-export const MAT_CHIPS_DEFAULT_OPTIONS = new InjectionToken(
- 'mat-chips-default-options',
-);
-
-/**
- * Injection token that can be used to reference instances of `MatChipAvatar`. It serves as
- * alternative token to the actual `MatChipAvatar` class which could cause unnecessary
- * retention of the class and its directive metadata.
- */
-export const MAT_CHIP_AVATAR = new InjectionToken('MatChipAvatar');
-
-/**
- * Injection token that can be used to reference instances of `MatChipTrailingIcon`. It serves as
- * alternative token to the actual `MatChipTrailingIcon` class which could cause unnecessary
- * retention of the class and its directive metadata.
- */
-export const MAT_CHIP_TRAILING_ICON = new InjectionToken('MatChipTrailingIcon');
-
-/**
- * Injection token that can be used to reference instances of `MatChipRemove`. It serves as
- * alternative token to the actual `MatChipRemove` class which could cause unnecessary
- * retention of the class and its directive metadata.
- */
-export const MAT_CHIP_REMOVE = new InjectionToken('MatChipRemove');
-
-/**
- * Injection token used to avoid a circular dependency between the `MatChip` and `MatChipAction`.
- */
-export const MAT_CHIP = new InjectionToken('MatChip');