:has(): محدد العائلة

منذ أن بدأ الوقت (في مصطلح CSS)، عملنا باستخدام سلسلة متتالية من الجوانب المختلفة. تنشئ الأنماط الخاصة بنا "ورقة أنماط متتالية". وتتدرج محدداتنا أيضًا. يمكن أن تتحرك بانحراف. وفي معظم الحالات، تنخفض. ولكن لن يرتفع أداؤه أبدًا. على مدار السنوات الماضية، تخيلنا أداة اختيار الوالدين. والآن، لقد جاءت أخيرًا! على شكل أداة اختيار صوريّة :has()

تمثل فئة CSS الزائفة :has() عنصرًا إذا تطابق أي من أدوات الاختيار التي تم تمريرها كمعلَمات مع عنصر واحد على الأقل.

لكنه أكثر من مجرد "أب" أداة الاختيار. وهذه طريقة لطيفة لتسويقه. قد تكون الطريقة غير الجذابة هي "البيئة المشروطة" أداة الاختيار. لكن هذا ليس لديه نفس الرنين تمامًا. ماذا عن "العائلة" محدد؟

دعم المتصفح

قبل التطرق إلى أبعد من ذلك، تجدر الإشارة إلى دعم المتصفح. لم يتم الوصول إلى هناك بعد. لكنه يقترب. لا يتوفر دعم لبرنامج Firefox حتى الآن، لكن لا يزال هناك خارطة الطريق. ولكنه موجود بالفعل في Safari ومن موعد إطلاقه في Chromium 105. ستخبرك جميع العروض التوضيحية في هذه المقالة ما إذا كانت غير متوافقة مع المتصفح المستخدَم.

كيفية استخدام :has

كيف يبدو إذن الوصول؟ ضع في الاعتبار HTML التالي مع عنصرين تابعين للفئة everybody. كيف يمكنك اختيار الملف التابع للفئة a-good-time؟

<div class="everybody">
  <div>
    <div class="a-good-time"></div>
  </div>
</div>

<div class="everybody"></div>

باستخدام :has()، يمكنك إجراء ذلك باستخدام خدمة CSS التالية.

.everybody:has(.a-good-time) {
  animation: party 21600s forwards;
}

يؤدي هذا إلى اختيار المثيل الأول من .everybody وتطبيق animation.

في هذا المثال، العنصر الذي يتضمن الفئة everybody هو الهدف. يضم الشرط عنصرًا تابعًا للفئة a-good-time.

<target>:has(<condition>) { <styles> }

ولكن يمكنك الذهاب إلى أبعد من ذلك لأن :has() يفتح الكثير من الفرص. وحتى تلك التي لم يتم اكتشافها بعد. ضع في اعتبارك بعضًا منها.

اختَر عناصر figure التي تحتوي على figcaption مباشر. css figure:has(> figcaption) { ... } اختَر anchor التي لا تحتوي على عنصر تابع مباشر بتنسيق SVG. css a:not(:has(> svg)) { ... } اختَر label التي تتضمّن شقيقًا شقيقًا مباشرًا في input. تحرك بطريقة جانبية! css label:has(+ input) { … } يمكنك اختيار article حيث لا يحتوي عنصر img التابع على نص alt. css article:has(img:not([alt])) { … } اختَر documentElement الذي يتضمّن حالة معيّنة في DOM css :root:has(.menu-toggle[aria-pressed=”true”]) { … } اختَر حاوية التنسيق التي تتضمّن عددًا فرديًا من العناصر الثانوية css .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } اختيار جميع العناصر في الشبكة التي لم يتم تمريرها css .grid:has(.grid__item:hover) .grid__item:not(:hover) { ... } اختَر الحاوية التي تحتوي على عنصر مخصّص <todo-list>. css main:has(todo-list) { ... } تحديد كل a منفرد في فقرة تحتوي على عنصر شقيق مباشر hr css p:has(+ hr) a:only-child { … } اختَر article عند استيفاء شروط متعدّدة. css article:has(>h1):has(>h2) { … } ننصحك بخلط هذا المحتوى. اختيار article حيث يكون العنوان متبوعًا بعنوان فرعي css article:has(> h1 + h2) { … } اختيار :root عند تفعيل الحالات التفاعلية css :root:has(a:hover) { … } حدِّد الفقرة التي تلي figure التي لا تحتوي على figcaption. css figure:not(:has(figcaption)) + p { … }

ما هي حالات الاستخدام المثيرة للاهتمام التي يمكنك التفكير فيها بشأن :has()؟ الأمر الرائع هنا يشجعك على كسر نموذجك العقلي. إنه يجعلك تفكر "هل يمكنني التعامل مع هذه الأنماط بطريقة مختلفة؟".

أمثلة

لنستعرض بعض الأمثلة حول كيفية استخدامنا لهذه الأداة.

البطاقات

مشاهدة عرض توضيحي للبطاقة الكلاسيكية. ويمكننا عرض أي معلومات في البطاقة، على سبيل المثال: عنوان أو عنوان فرعي أو بعض الوسائط. إليك البطاقة الأساسية.

<li class="card">
  <h2 class="card__title">
      <a href="#">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
</li>

ماذا يحدث عندما تريد تقديم بعض الوسائط؟ بالنسبة لهذا التصميم، يمكن تقسيم البطاقة إلى عمودين. قبل ذلك، كان بإمكانك إنشاء صف جديد لتمثيل هذا السلوك، على سبيل المثال card--with-media أو card--two-columns. بالإضافة إلى ذلك، يصعُب العثور على أسماء الفئات هذه، كما يصبح من الصعب الحفاظ عليها وتذكّرها.

من خلال ":has()"، يمكنك رصد ما إذا كانت البطاقة تحتوي على بعض الوسائط وتنفيذ الإجراء المناسب. ولا حاجة إلى أسماء فئات التعديل.

<li class="card">
  <h2 class="card__title">
    <a href="/article.html">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
  <img
    class="card__media"
    alt=""
    width="400"
    height="400"
    src="./team-awesome.png"
  />
</li>

وليس عليك تركه هناك. يمكنك استخدامه بطريقة إبداعية. كيف يمكن أن تتكيف بطاقة تعرض محتوى "مميزًا" ضمن التخطيط؟ ستجعل CSS هذه البطاقة المميزة بالعرض الكامل للتخطيط وتضعها في بداية الشبكة.

.card:has(.card__banner) {
  grid-row: 1;
  grid-column: 1 / -1;
  max-inline-size: 100%;
  grid-template-columns: 1fr 1fr;
  border-left-width: var(--size-4);
}

ماذا لو كانت بطاقة مميّزة مع بانر يهتز لجذب الانتباه؟

<li class="card">
  <h2 class="card__title">
    <a href="#">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
  <img
    class="card__media"
    alt=""
    width="400"
    height="400"
    src="./team-awesome.png"
  />
  <div class="card__banner"></div>
</li>
.card:has(.card__banner) {
  --color: var(--green-3-hsl);
  animation: wiggle 6s infinite;
}

هناك الكثير من الاحتمالات.

النماذج

ماذا عن النماذج؟ وهي تشتهر بصعوبة الأسلوب. وأحد الأمثلة على ذلك هو تصميم الإدخالات والتصنيفات الخاصة بها. كيف يمكننا الإشارة إلى أنّ أحد الحقول صالح على سبيل المثال؟ باستخدام :has()، سيصبح هذا الإجراء أسهل بكثير. ويمكننا جذب المحتوى من خلال الفئات الزائفة ذات الصلة، على سبيل المثال :valid و:invalid.

<div class="form-group">
  <label for="email" class="form-label">Email</label>
  <input
    required
    type="email"
    id="email"
    class="form-input"
    title="Enter valid email address"
    placeholder="Enter valid email address"
  />   
</div>
label {
  color: var(--color);
}
input {
  border: 4px solid var(--color);
}

.form-group:has(:invalid) {
  --color: var(--invalid);
}

.form-group:has(:focus) {
  --color: var(--focus);
}

.form-group:has(:valid) {
  --color: var(--valid);
}

.form-group:has(:placeholder-shown) {
  --color: var(--blur);
}

جرِّب ذلك في هذا المثال: جرِّب إدخال قيم صالحة وغير صالحة مع التركيز على النص وإيقافه.

يمكنك أيضًا استخدام :has() لإظهار رسالة الخطأ الخاصة بأحد الحقول وإخفائها. أدخِل مجموعة حقل "البريد الإلكتروني" وأضِف رسالة خطأ إليها.

<div class="form-group">
  <label for="email" class="form-label">
    Email
  </label>
  <div class="form-group__input">
    <input
      required
      type="email"
      id="email"
      class="form-input"
      title="Enter valid email address"
      placeholder="Enter valid email address"
    />   
    <div class="form-group__error">Enter a valid email address</div>
  </div>
</div>

وبشكلٍ تلقائي، تخفي رسالة الخطأ.

.form-group__error {
  display: none;
}

ولكن عندما يصبح الحقل :invalid ولا يتم التركيز عليه، يمكنك عرض الرسالة بدون الحاجة إلى أسماء إضافية للفئات.

.form-group:has(:invalid:not(:focus)) .form-group__error {
  display: block;
}

ولا داعي للقلق إذا استطعت إضافة لمسة غير تقليدية على ذوقك عند تفاعل المستخدمين مع النموذج. فكّر في هذا المثال. راقب ما يحدث عند إدخال قيمة صالحة للتفاعل المصغّر. ستؤدي قيمة :invalid إلى اهتزاز مجموعة النماذج. ولكن، فقط إذا لم يكن لدى المستخدم تفضيلات للحركة.

المحتوى

وقد تطرقنا إلى ذلك في أمثلة التعليمات البرمجية. كيف يمكنك استخدام :has() في مسار المستند؟ حيث تطرح أفكارًا حول كيفية تصميم أسلوب الخط حول الوسائط على سبيل المثال.

figure:not(:has(figcaption)) {
  float: left;
  margin: var(--size-fluid-2) var(--size-fluid-2) var(--size-fluid-2) 0;
}

figure:has(figcaption) {
  width: 100%;
  margin: var(--size-fluid-4) 0;
}

figure:has(figcaption) img {
  width: 100%;
}

يحتوي هذا المثال على أشكال. وعندما لا تحتوي على figcaption، تطفو داخل المحتوى. عند عرض figcaption، تشغل العرض الكامل وتحصل على هامش إضافي.

التفاعل مع الحالة

ما رأيك بجعل الأنماط تتفاعل مع بعض الحالات في ترميزنا. ضع في الاعتبار مثالاً باسم "الكلاسيكي" تمرير شريط التنقل. إذا كان لديك زر يعمل على فتح شريط التنقّل، قد يستخدم السمة aria-expanded. يمكن استخدام JavaScript لتحديث السمات المناسبة. عندما تكون قيمة aria-expanded هي true، يمكنك استخدام :has() لرصد ذلك وتعديل أنماط شريط التنقّل بالتمرير. تؤدي لغة JavaScript دورها ويمكن لـ CSS تنفيذ ما يريد باستخدام هذه المعلومات. ولا حاجة إلى تبديل الترميز أو إضافة أسماء فئات إضافية وما إلى ذلك (ملاحظة: هذا ليس مثالاً على جاهزٍ للإصدار العلني).

:root:has([aria-expanded="true"]) {
    --open: 1;
}
body {
    transform: translateX(calc(var(--open, 0) * -200px));
}

هل يمكن أن يساعد في تجنب أخطاء المستخدم؟

ما القاسم المشترك بين كل هذه الأمثلة؟ وبالإضافة إلى عرض طرق لاستخدام :has()، لم يتطلب أيّ منها تعديل أسماء الفئات. وأدرج كل منهم محتوى جديدًا وحدث سمة. هذه فائدة رائعة لتطبيق :has()، إذ يمكن أن يساعد في الحدّ من أخطاء المستخدم. من خلال :has()، سيكون بإمكان CSS تحمّل مسؤولية تعديل التعديلات في نموذج العناصر في المستند (DOM). ولن تحتاج إلى التوفيق بين أسماء الفئات في JavaScript، ما يقلّل من احتمال حدوث أخطاء مطوّري البرامج. كلنا طبّقنا الخطأ نفسه على اسم الصف واضطررنا إلى الاحتفاظ به في عمليات بحث Object.

إنها فكرة مثيرة للاهتمام وهل تساعدنا على تحسين الترميز واستخدام رموز أقل؟ يجب تقليل حجم JavaScript لأنّنا لا نُجري العديد من تعديلات JavaScript. يمكنك تقليل محتوى HTML لأنّك لم تعُد بحاجة إلى صفوف مثل "card card--has-media" وغير ذلك.

التفكير خارج الصندوق

كما ذكرنا أعلاه، يشجعك تطبيق ":has()" على القضاء على النموذج العقلي. وهي فرصة لتجربة أشياء مختلفة. يمكنك محاولة تخطِّي الحواجز من خلال تطوير آليات اللعب باستخدام CSS فقط. وعلى سبيل المثال، يمكنك إنشاء آلية مستندة إلى الخطوات باستخدام النماذج وCSS.

<div class="step">
  <label for="step--1">1</label>
  <input id="step--1" type="checkbox" />
</div>
<div class="step">
  <label for="step--2">2</label>
  <input id="step--2" type="checkbox" />
</div>
.step:has(:checked), .step:first-of-type:has(:checked) {
  --hue: 10;
  opacity: 0.2;
}


.step:has(:checked) + .step:not(.step:has(:checked)) {
  --hue: 210;
  opacity: 1;
}

وهذا يفتح إمكانيات مثيرة للاهتمام. يمكنك استخدام ذلك لاجتياز نموذج باستخدام التحويلات. ملاحظة: إنّ أفضل عرض لهذا العرض التوضيحي هو علامة تبويب متصفّح منفصلة.

من أجل المتعة، ماذا عن لعبة الأسلاك الكلاسيكية مكبِّرة الصوت؟ الميكانيكي أسهل في الإنشاء باستخدام :has(). إذا تم تمرير مؤشر الماوس فوق السلك، تنتهي اللعبة. نعم، يمكننا إنشاء بعض آليات الألعاب هذه باستخدام عناصر مثل المدمجات التابعة (+ و~). مع ذلك، تُعدّ :has() طريقة لتحقيق النتائج نفسها بدون الحاجة إلى استخدام ترميز "حيل" مثير للاهتمام. ملاحظة: إنّ أفضل عرض لهذا العرض التوضيحي هو علامة تبويب متصفّح منفصلة.

على الرغم من أنك لن تطرحها في مرحلة الإنتاج في أي وقت قريب، فهي تسلط الضوء على الطرق التي يمكنك من خلالها استخدام الطريقة الأساسية. مثل القدرة على ربط :has().

:root:has(#start:checked):has(.game__success:hover, .screen--win:hover)
.screen--win {
  --display-win: 1;
}

الأداء والقيود

قبل أن ننتهي، ما هي الإجراءات التي لا يمكنك تنفيذها على ":has()"؟ هناك بعض القيود على :has(). تنشأ العوامل الرئيسية بسبب نتائج الأداء.

  • لا يمكنك :has() :has(). ولكن يمكنك ربط ":has()" بالسلسلة. css :has(.a:has(.b)) { … }
  • لن يتم استخدام العنصر الزائف في :has(). css :has(::after) { … } :has(::first-letter) { … }
  • حظر استخدام :has() داخل الصور الزائفة التي تقبل أدوات الاختيار المركّبة فقط css ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … }
  • حظر استخدام :has() بعد العنصر الزائف css ::part(foo):has(:focus) { … }
  • لن يتم استخدام القيمة :visited دائمًا. css :has(:visited) { … }

للاطّلاع على مقاييس الأداء الفعلية ذات الصلة بـ :has()، يمكنك الاطّلاع على هذا الخطأ. وننسبه إلى Byungwoo لمشاركة هذه الإحصاءات والتفاصيل حول عملية التنفيذ.

هذا كل شيء.

استعِدّ لـ :has(). يمكنك إطلاع أصدقائك على هذا الإصدار من خلال مشاركة هذه المشاركة، فستساعدنا في تحسين طريقة تعاملنا مع CSS.

تتوفّر جميع العروض التوضيحية في مجموعة CodePen.