: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) { … } اختيار articles حيث لا يحتوي عنصر تابع img على altarticlecss article:has(img:not([alt])) { … }documentElementcss :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) { ... }ahrcss p:has(+ hr) a:only-child { … }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 هذه.