Тень ДОМ 201

CSS и стили

В этой статье рассказывается о других удивительных вещах, которые можно сделать с помощью Shadow DOM. Он основан на концепциях, обсуждавшихся в Shadow DOM 101 . Если вы ищете введение, см. эту статью.

Введение

Давайте посмотрим правде в глаза. В нестилизованной разметке нет ничего привлекательного. К счастью для нас, блестящие люди из веб-компонентов предвидели это и не оставили нас в замешательстве. Модуль CSS Scoping определяет множество опций для стилизации содержимого в теневом дереве.

Инкапсуляция стилей

Одной из основных особенностей Shadow DOM является граница тени . У него много хороших свойств, но одним из лучших является то, что он обеспечивает бесплатную инкапсуляцию стилей. Сформулировано иначе:

<div><h3>Light DOM</h3></div>
<script>
var root = document.querySelector('div').createShadowRoot();
root.innerHTML = `
  <style>
    h3 {
      color: red;
    }
  </style>
  <h3>Shadow DOM</h3>
`;
</script>

Есть два интересных наблюдения по поводу этой демонстрации:

  • На этой странице есть и другие h3, но единственный, который соответствует селектору h3 и, следовательно, имеет красный цвет, — это тот, который находится в ShadowRoot. Опять же, стили с ограниченной областью действия по умолчанию.
  • Другие правила стилей, определенные на этой странице и ориентированные на h3, не проникают в мой контент. Это потому, что селекторы не пересекают границу тени .

Мораль истории? У нас есть инкапсуляция стилей от внешнего мира. Спасибо Шэдоу ДОМ!

Стилизация главного элемента

:host позволяет вам выбрать и стилизовать элемент, содержащий теневое дерево:

<button class="red">My Button</button>
<script>
var button = document.querySelector('button');
var root = button.createShadowRoot();
root.innerHTML = `
  <style>
    :host {
      text-transform: uppercase;
    }
  </style>
  <content></content>
`;
</script>

Одна из ошибок заключается в том, что правила на родительской странице имеют более высокую специфичность, чем правила :host , определенные в элементе, но более низкую специфичность, чем атрибут style , определенный в элементе хоста. Это позволяет пользователям переопределить ваш стиль снаружи. :host также работает только в контексте ShadowRoot, поэтому вы не можете использовать его за пределами Shadow DOM.

Функциональная форма :host(<selector>) позволяет выбрать элемент хоста, если он соответствует <selector> .

Пример : совпадение только в том случае, если сам элемент имеет класс .different (например <x-foo class="different"></x-foo> ):

:host(.different) {
    ...
}

Реагирование на состояния пользователя

Типичный вариант использования :host — это когда вы создаете пользовательский элемент и хотите реагировать на различные состояния пользователя (:hover, :focus, :active и т. д.).

<style>
  :host {
    opacity: 0.4;
    transition: opacity 420ms ease-in-out;
  }
  :host(:hover) {
    opacity: 1;
  }
  :host(:active) {
    position: relative;
    top: 3px;
    left: 3px;
  }
</style>

Тематическое оформление элемента

Псевдокласс :host-context(<selector>) соответствует элементу хоста, если он или любой из его предков соответствует <selector> .

Обычно :host-context() используется для оформления элемента на основе его окружения. Например, многие люди создают темы, применяя класс к <html> или <body> :

<body class="different">
  <x-foo></x-foo>
</body>

Вы можете :host-context(.different) стилизовать <x-foo> , если он является потомком элемента с классом .different :

:host-context(.different) {
  color: red;
}

Это дает вам возможность инкапсулировать правила стиля в Shadow DOM элемента, которые уникально стилизуют его в зависимости от его контекста.

Поддержка нескольких типов хостов из одного теневого корня

Другое использование :host — если вы создаете библиотеку тем и хотите поддерживать стилизацию многих типов хост-элементов внутри одного и того же Shadow DOM.

:host(x-foo) {
    /* Applies if the host is a <x-foo> element.*/
}

:host(x-foo:host) {
    /* Same as above. Applies if the host is a <x-foo> element. */
}

:host(div) {
    /* Applies if the host element is a <div>. */
}

Стилизация внутренних элементов Shadow DOM снаружи

Псевдоэлемент ::shadow и комбинатор /deep/ подобны мечу Vorpal, обладающему авторитетом CSS. Они позволяют проникать за границы Shadow DOM и стилизовать элементы внутри теневых деревьев.

Псевдоэлемент ::shadow

Если элемент имеет хотя бы одно теневое дерево, псевдоэлемент ::shadow соответствует самому теневому корню. Это позволяет вам писать селекторы, которые стилизуют узлы внутри теневой области элемента.

Например, если элемент содержит теневой корень, вы можете написать #host::shadow span {} , чтобы стилизовать все диапазоны в его теневом дереве.

<style>
  #host::shadow span {
    color: red;
  }
</style>

<div id="host">
  <span>Light DOM</span>
</div>

<script>
  var host = document.querySelector('div');
  var root = host.createShadowRoot();
  root.innerHTML = `
    <span>Shadow DOM</span>
    <content></content>
  `;
</script>

Пример (пользовательские элементы) — <x-tabs> имеет дочерние элементы <x-panel> в своем Shadow DOM. На каждой панели имеется собственное теневое дерево, содержащее заголовки h2 . Чтобы стилизовать эти заголовки на главной странице, можно написать:

x-tabs::shadow x-panel::shadow h2 {
    ...
}

Комбинатор /deep/

Комбинатор /deep/ похож на ::shadow , но более мощный. Он полностью игнорирует все границы теней и переходит в любое количество теневых деревьев. Проще говоря, /deep/ позволяет вам глубже проникнуть в суть элемента и нацелиться на любой узел.

Комбинатор /deep/ особенно полезен в мире пользовательских элементов, где часто используется несколько уровней Shadow DOM. Яркими примерами являются вложение группы пользовательских элементов (каждый из которых содержит собственное теневое дерево) или создание элемента, который наследуется от другого, с помощью <shadow> .

Пример (пользовательские элементы) — выберите все элементы <x-panel> , которые являются потомками <x-tabs> , в любом месте дерева:

x-tabs /deep/ x-panel {
    ...
}

Пример : стилизовать все элементы с помощью класса .library-theme в любом месте теневого дерева:

body /deep/ .library-theme {
    ...
}

Работа с querySelector()

Точно так же, как .shadowRoot открывает деревья теней для обхода DOM, комбинаторы открывают деревья теней для обхода селектора. Вместо написания вложенной цепочки безумия вы можете написать один оператор:

// No fun.
document.querySelector('x-tabs').shadowRoot
        .querySelector('x-panel').shadowRoot
        .querySelector('#foo');

// Fun.
document.querySelector('x-tabs::shadow x-panel::shadow #foo');

Стилизация нативных элементов

Собственные элементы управления HTML — сложная задача для стилизации. Многие люди просто сдаются и скатываются самостоятельно. Однако с помощью ::shadow и /deep/ можно стилизовать любой элемент веб-платформы, использующий Shadow DOM. Отличными примерами являются типы <input> и <video> :

video /deep/ input[type="range"] {
  background: hotpink;
}

Создание хуков стиля

Кастомизация это хорошо. В некоторых случаях вы можете захотеть проделать дыры в защитном щитке для стилизации вашего Shadow и создать крючки, которые другие смогут стилизовать.

Использование ::shadow и /deep/

За /deep/ кроется много силы. Это дает авторам компонентов возможность обозначить отдельные элементы как стилизуемые или множество элементов как тематические.

Пример — стиль всех элементов, имеющих класс .library-theme , игнорируя все теневые деревья:

body /deep/ .library-theme {
    ...
}

Использование пользовательских псевдоэлементов

И WebKit , и Firefox определяют псевдоэлементы для стилизации внутренних частей собственных элементов браузера. Хорошим примером является input[type=range] . Вы можете оформить ползунок <span style="color:blue">blue</span> нацелив ::-webkit-slider-thumb :

input[type=range].custom::-webkit-slider-thumb {
  -webkit-appearance: none;
  background-color: blue;
  width: 10px;
  height: 40px;
}

Подобно тому, как браузеры предоставляют возможности стилизации в некоторых внутренних компонентах, авторы контента Shadow DOM могут обозначить определенные элементы как стилизованные для посторонних. Это делается посредством пользовательских псевдоэлементов .

Вы можете обозначить элемент как пользовательский псевдоэлемент, используя pseudo . Его значение или имя должно иметь префикс «x-». Это создает связь с этим элементом в теневом дереве и дает посторонним выделенную полосу для пересечения границы тени.

Вот пример создания пользовательского виджета-слайдера и предоставления кому-либо возможности оформить его ползунок в синий цвет:

<style>
  #host::x-slider-thumb {
    background-color: blue;
  }
</style>
<div id="host"></div>
<script>
  var root = document.querySelector('#host').createShadowRoot();
  root.innerHTML = `
    <div>
      <div pseudo="x-slider-thumb"></div>' +
    </div>
  `;
</script>

Использование переменных CSS

Мощным способом создания хуков тем является использование переменных CSS . По сути, создание «заполнителей стиля», которые могут заполнить другие пользователи.

Представьте себе автора пользовательского элемента, который выделяет заполнители переменных в своем Shadow DOM. Один для оформления шрифта внутренней кнопки, а другой — для ее цвета:

button {
  color: var(--button-text-color, pink); /* default color will be pink */
  font-family: var(--button-font);
}

Затем специалист по внедрению элемента определяет эти значения по своему вкусу. Возможно, чтобы соответствовать супер крутой теме Comic Sans на их собственной странице:

#host {
  --button-text-color: green;
  --button-font: "Comic Sans MS", "Comic Sans", cursive;
}

Благодаря способу наследования переменных CSS все отлично и прекрасно работает! Вся картина выглядит так:

<style>
  #host {
    --button-text-color: green;
    --button-font: "Comic Sans MS", "Comic Sans", cursive;
  }
</style>
<div id="host">Host node</div>
<script>
  var root = document.querySelector('#host').createShadowRoot();
  root.innerHTML = `
    <style>
      button {
        color: var(--button-text-color, pink);
        font-family: var(--button-font);
      }
    </style>
    <content></content>
  `;
</script>

Сброс стилей

Наследуемые стили, такие как шрифты, цвета и высота строк, продолжают влиять на элементы в Shadow DOM. Однако для максимальной гибкости Shadow DOM предоставляет нам свойство resetStyleInheritance для управления тем, что происходит на границе тени. Думайте об этом как о способе начать все заново при создании нового компонента.

сбросстиленаследование

  • false — значение по умолчанию. наследуемые свойства CSS продолжают наследовать.
  • true — сбрасывает наследуемые свойства до initial на границе.

Ниже приведена демонстрация, показывающая, как изменение resetStyleInheritance влияет на теневое дерево:

<div>
  <h3>Light DOM</h3>
</div>

<script>
  var root = document.querySelector('div').createShadowRoot();
  root.resetStyleInheritance = <span id="code-resetStyleInheritance">false</span>;
  root.innerHTML = `
    <style>
      h3 {
        color: red;
      }
    </style>
    <h3>Shadow DOM</h3>
    <content select="h3"></content>
  `;
</script>

<div class="demoarea" style="width:225px;">
  <div id="style-ex-inheritance"><h3 class="border">Light DOM</div>
</div>
<div id="inherit-buttons">
  <button id="demo-resetStyleInheritance">resetStyleInheritance=false</button>
</div>

<script>
  var container = document.querySelector('#style-ex-inheritance');
  var root = container.createShadowRoot();
  //root.resetStyleInheritance = false;
  root.innerHTML = '<style>h3{ color: red; }</style><h3>Shadow DOM<content select="h3"></content>';

  document.querySelector('#demo-resetStyleInheritance').addEventListener('click', function(e) {
    root.resetStyleInheritance = !root.resetStyleInheritance;
    e.target.textContent = 'resetStyleInheritance=' + root.resetStyleInheritance;
    document.querySelector('#code-resetStyleInheritance').textContent = root.resetStyleInheritance;
  });
</script>
Унаследованные свойства DevTools

Понимание .resetStyleInheritance немного сложнее, прежде всего потому, что он влияет только на наследуемые свойства CSS. Там говорится: когда вы ищете свойство для наследования на границе между страницей и ShadowRoot, не наследуйте значения от хоста, а вместо этого используйте initial значение (согласно спецификации CSS).

Если вы не уверены в том, какие свойства наследуются в CSS, ознакомьтесь с этим удобным списком или установите флажок «Показать унаследованные» на панели «Элемент».

Стилизация распределенных узлов

Распределенные узлы — это элементы, которые отображаются в точке вставки (элемент <content> ). Элемент <content> позволяет выбирать узлы из Light DOM и отображать их в заранее определенных местах в Shadow DOM. Логически они не находятся в Shadow DOM; они все еще являются дочерними элементами основного элемента. Точки вставки — это всего лишь вещь рендеринга.

Распределенные узлы сохраняют стили из основного документа. То есть правила стиля с главной страницы продолжают применяться к элементам, даже если они отображаются в точке вставки. Опять же, распределенные узлы по-прежнему логически находятся в светлом пространстве и не перемещаются. Они просто рендерятся в другом месте. Однако когда узлы распределяются в Shadow DOM, они могут принимать дополнительные стили, определенные внутри теневого дерева.

::псевдоэлемент содержимого

Распределенные узлы являются дочерними элементами хост-элемента, так как мы можем нацелить их из теневого DOM? Ответ — псевдоэлемент CSS ::content . Это способ нацеливаться на узлы Light DOM, которые проходят через точку вставки. Например:

::content > h3 стилизует любые теги h3 , проходящие через точку вставки.

Давайте посмотрим пример:

<div>
  <h3>Light DOM</h3>
  <section>
    <div>I'm not underlined</div>
    <p>I'm underlined in Shadow DOM!</p>
  </section>
</div>

<script>
var div = document.querySelector('div');
var root = div.createShadowRoot();
root.innerHTML = `
  <style>
    h3 { color: red; }
      content[select="h3"]::content > h3 {
      color: green;
    }
    ::content section p {
      text-decoration: underline;
    }
  </style>
  <h3>Shadow DOM</h3>
  <content select="h3"></content>
  <content select="section"></content>
`;
</script>

Сброс стилей в точках вставки

При создании ShadowRoot у вас есть возможность сбросить унаследованные стили. Точки вставки <content> и <shadow> также имеют эту опцию. При использовании этих элементов либо установите .resetStyleInheritance в JS, либо используйте логический атрибут reset-style-inheritance для самого элемента.

  • Для точек вставки ShadowRoot или <shadow> : reset-style-inheritance означает, что наследуемым свойствам CSS присваиваются initial значения на хосте, прежде чем они попадут в ваш теневой контент. Это место известно как верхняя граница .

  • Для точек вставки <content> : reset-style-inheritance означает, что наследуемым свойствам CSS присваиваются initial до того, как дочерние элементы хоста будут распределены в точке вставки. Это место известно как нижняя граница .

Заключение

Как авторы пользовательских элементов, у нас есть масса возможностей контролировать внешний вид нашего контента. Shadow DOM формирует основу этого дивного нового мира.

Shadow DOM дает нам инкапсуляцию стилей с ограниченной областью действия и средства, позволяющие впускать столько (или столько) внешнего мира, сколько мы захотим. Определяя пользовательские псевдоэлементы или включая заполнители переменных CSS, авторы могут предоставлять сторонним удобным средствам стилизации для дальнейшей настройки своего контента. В общем, веб-авторы полностью контролируют представление своего контента.