Такие языки дизайна, как Material Design, предоставляют разработчикам строительные блоки, помогая нам создавать красивый и интуитивно понятный дизайн. Вы можете найти компоненты любого типа, от простых кнопок и карточек до средств выбора даты и сложных таблиц данных. Он покрывает большинство потребностей разработчика, но недавно я столкнулся с требованием UX, для которого я не мог найти ни одного подходящего компонента, даже с небольшой настройкой.

Я хотел отображать дополнительную информацию на карточке, когда пользователь нажимает значок «информация»:

Эта панель должна закрываться, если пользователь щелкает за ее пределами, и оставаться в положении относительно ее метки, даже если пользователь прокручивает страницу вниз. Мы также хотим иметь возможность встраивать компоненты во всплывающее окно, а не только отображать текст. Одно решение может быть создано с использованием overlay пакета CDK.

Если вам нужно только окончательное решение, вы можете проверить это репо. В этой статье мы рассмотрим основные моменты реализации.

Настраивать

Если вы уже используете Angular Material в своем проекте, вы настроены и можете пропустить этот раздел. Если нет, у вас есть два варианта:

  • Установите и настройте только Angular Material CDK
  • Установите и настройте Angular Material (рекомендуется для нашего примера)

Установите и настройте Angular Material CDK

Вам не нужно устанавливать полный пакет Angular Material, чтобы использовать пакет overlay. Однако для информационных всплывающих окон в этом примере нам понадобятся компоненты MatIcon и MatCard из Angular Material. Но вы можете технически заменить эти компоненты соответственно другим значком и самодельной картой, чтобы вы могли установить CDK самостоятельно:

npm i @angular/cdk --save

и импортируйте следующую строку в свой глобальный стиль:

@import '~@angular/cdk/overlay-prebuilt.css';

Установить и настроить Angular Material

Поскольку мы собираемся использовать в примере MatCard и MatIcon, это рекомендуемый вариант, если вы хотите писать код. Вы можете найти полное руководство здесь, но для быстрой настройки просто запустите:

ng add @angular/material

и убедитесь, что BrowserAnimationsModule импортирован в ваш app.module.ts.

NB: Как уже упоминалось, нам понадобятся MatCardModule и MatIconModule. Поскольку вы находитесь в своем app.module.ts, вы уже можете импортировать их сейчас.

Директива

Начнем с создания директивы:

Мы хотим иметь возможность легко добавлять это всплывающее окно к любой комбинации ярлыка / кнопки. Здесь есть разные стратегии, чтобы сделать его многоразовым и настраиваемым. Мы могли бы использовать структурную директиву для метки, которая автоматически добавляла бы значок, но это затруднило бы настройку выбора кнопки и ее положения. Было решено, что кнопка должна быть добавлена ​​вручную и что директива на ней будет определять поведение всплывающего окна.

Компонент app-info-button представляет собой простую иконку:

Он оформлен и генерирует событие при нажатии, но не участвует в поведении всплывающего окна.

Мы добавляем кнопку рядом с меткой с прикрепленной к ней директивой appInfoPopup. Директива принимает в качестве входных данных шаблон, который содержит информацию, которую мы хотим отобразить во всплывающей карточке. Директива также принимает дополнительный параметр label, который является элементом, к которому будет прикреплено всплывающее окно. В примере в начале статьи вы можете видеть, что всплывающее окно расположено относительно «Some Label», а не значка.

Наше всплывающее окно полностью настраивается: мы можем выбрать, с каким элементом оно связано, полностью свободны в том, что мы отображаем в нем, и можем решить, какая кнопка его открывает / закрывает.

Оверлей

Теперь нам нужно добавить логику в нашу директиву. Директива сначала должна создать элемент оверлея с функцией overlay.create(). Эта функция возвращает экземпляр OverlayRef, который мы сможем прикрепить и отсоединить.

Мы можем передать несколько параметров функции create. Вы можете найти полный список опций в документации API, но для нашего случая использования мы собираемся использовать только некоторые из них.

Мы можем определить две важные части поведения наложения: как оно позиционируется и что происходит, когда пользователь прокручивает страницу.

Стратегия позиции

Есть два способа позиционирования ваших наложений: они могут быть размещены глобально в области просмотра или они могут быть связаны с элементом. Мы хотим использовать вторую стратегию: наше всплывающее окно должно располагаться относительно элемента label.

const positionStrategy = this.overlay
  .position()
  .flexibleConnectedTo(this.label);

Мы все еще можем указать, где именно мы хотим, чтобы оно было: сверху, слева, справа ... Но не всегда может быть достаточно места для размещения всплывающего окна, как мы его указали, поэтому мы можем передать список предпочтительных положений, от наиболее предпочтительного до наименее предпочтительного:

const positionStrategy = this.overlay
  .position()
  .flexibleConnectedTo(this.label)
  .withPositions([
     new ConnectionPositionPair(
       { originX: 'start', originY: 'bottom' },
       { overlayX: 'start', overlayY: 'top' },
     ),
     new ConnectionPositionPair(
       { originX: 'start', originY: 'top' },
       { overlayX: 'start', overlayY: 'bottom' },
     ),
  ])
  .withPush(false);

Синтаксис ConnectionPositionPair может показаться немного сложным, но давайте расшифруем первый: он сообщает, какая точка наложения и какая точка origin, также известная как label, должны быть общими. Линия { originX: 'start', originY: 'bottom' } означает, что наложение будет расположено относительно нижнего левого угла этикетки. Строкой { overlayX: 'start', overlayY: 'top' } мы говорим, что более конкретно верхний левый угол наложения должен быть выровнен с контрольной точкой origin. Эта конфигурация приведет к положению, как на изображении в начале статьи. Если же места под лейблом не хватает, нам нужен запасной вариант. Это второй ConnectionPositionPair объект. Если это нужно использовать, нижний левый угол наложения будет выровнен с верхним левым углом ярлыка.

Вы могли заметить последний вариант, withPush. Если бы он был установлен на true, это означало бы, что CDK может принудительно выдвинуть оверлей на экран, если ни одно из предпочтительных положений не подходит. В нашем случае мы можем установить значение false.

Стратегия прокрутки

Мы также можем определить, как наложение взаимодействует с прокруткой. Есть четыре варианта:

  • noop: ничего не происходит, когда пользователь прокручивает, оверлей остается на месте. Мы не хотим этого, так как положение метки, вероятно, будет зависеть от прокрутки, и мы хотим, чтобы наложение следовало за ней.
  • block: вообще запретит пользователю прокрутку. Это полезно, когда ваше наложение представляет собой важное сообщение в центре экрана.
  • close: мы можем сказать, что наложение закрывается, как только пользователь прокручивает.
  • reposition: это то поведение, которое мы хотим. Пользователь может прокручивать, и наложение будет перемещаться относительно метки.
const scrollStrategy = this.overlay.scrollStrategies.reposition();

Создание наложения

Теперь, когда мы определили позицию и стратегии прокрутки, мы можем создать наложение:

this.overlayRef = this.overlay.create({
  positionStrategy,
  scrollStrategy,
  hasBackdrop: true,
  backdropClass: 'cdk-overlay-transparent-backdrop',
});

Два последних варианта также важны. Когда hasBackdrop равно true, наложение имеет элемент фона, который представляет собой элемент, отображаемый под наложением поверх всего окна просмотра. Позже мы увидим, зачем нам это нужно. Последний вариант указывает, что фон должен быть прозрачным, так как по умолчанию фон имеет прозрачный серый цвет, что делает фон отключенным.

Прикрепить / отсоединить накладку

Теперь наложение создано, но пока ничего не видно. Мы хотим, чтобы всплывающее окно отображалось с контентом, который мы указали в качестве аргумента директивы, когда пользователь нажимает кнопку. В хуке жизненного цикла afterViewInit мы слушаем щелчки по кнопке

ngAfterViewInit(): void {          
  this.infoButton.infoButtonClicked
    .asObservable()
    .subscribe(() => {
       this.attachOverlay();
    });
 }
private attachOverlay(): void {
  if (!this.overlayRef.hasAttached()) {
  const periodSelectorPortal = new TemplatePortal(
    this.appInfoPopup,
    this.vcr,
  );
  this.overlayRef.attach(periodSelectorPortal);
}

Когда пользователь нажимает кнопку, мы создаем портал из информационного шаблона и прикрепляем его к overlayRef. На этом этапе появляется всплывающее окно.

Наконец, мы должны заставить его исчезать, когда пользователь щелкает за его пределами. Вот почему нам понадобился фон. Мы можем напрямую прослушивать щелчки фона и закрывать оверлей:

this.overlayRef
  .backdropClick()
  .pipe(takeUntil(this.unsubscribe))
  .subscribe(() => {
    this.detachOverlay();
  });
private detachOverlay(): void {
  if (this.overlayRef.hasAttached()) {
    this.overlayRef.detach();
  }
}

Вот полная директива:

Как упоминалось ранее, вы можете найти полный код в этом репозитории. Я надеюсь, что выполнение шагов по созданию и отображению всплывающего окна дало вам лучшее понимание пакета наложения CDK. Не стесняйтесь задавать вопросы здесь или в репозитории.