Інклюзивні компоненти: картки

23 хв. читання

Деякі з компонентів, які розглядаються в блозі Хейдона Пікерінґа, мають задокументовані рекомендації для коректної роботи. Наприклад, стандарт WAI-ARIA передбачає певну структуру і поведінку для вкладок. Наскільки точно слідувати цим рекомендаціям — залежить від вас. Може виявитися, що ваша аудиторія погано взаємодіє з інтерфейсом вкладки, який ви реалізували суворо за рекомендацією. Проте такі стандарти існують.

Інші компоненти, зокрема всюдисущі й різноманітні картки, не мають затвердженого стандарту. Не існує елементу <card> чи варіанту розмітки для «ARIA card». Працювати над такими компонентами найцікавіше. Кожен потенційний бар'єр для інклюзії потрібно виявляти й усувати по черзі.

Ці бар'єри відрізняються залежно від цілі та змісту картки. Деякі картки — лише ілюстрації для постів у блозі, інші ж мають більшу автономію і ширший функціонал. У цій статті розглядається кілька операцій із компонентом простої картки; особлива увага приділяється балансу між надійною HTML-структурою та зручною взаємодією.

Уявімо звичайну картку. Вона складається з ілюстрації, заголовка, опису й атрибуції. Важливо, що вона є елементом списку — як і в грі в карти — ви рідко маєте справу лише з одним екземпляром. А відомо, що використання списків поліпшує досвід користування допоміжними технологіями. Якщо коротко:

  • Скрінрідери мають комбінації клавіш для переходу між списками та їхніми елементами.
  • Скрінрідери підраховують елементи списку, щоби користувачі розуміли, скільки з них доступні.
Інклюзивні компоненти: картки
Список з чотирьох елементів

У цьому випадку картка — тізер до посту в блозі. Зверніть увагу на заголовок: як і розмітка списку, заголовок дає навігаційні підказки користувачам скрінрідерів. Усі картки мають заголовки одного рівня — у нашому випадку <h2> — тому що вони належать до ієрархії простого списку. Також зверніть увагу, що в цьому прикладі зображення відіграє декоративну роль і скрінрідер його проігнорує завдяки пустому значенню alt. Ми розглянемо позитивне значення alt у цій статті трохи пізніше.

<li>  
    <img src="/path/to/image.png" alt="">
    <h2>Card design woes</h2>
    <p>Ten common pitfalls to avoid when designing card components.</p>
    <small>By Heydon Pickering</small>
</li> 

Питання полягає в тому, куди помістити посилання на пост у блозі? Яка частина картки має бути інтерактивною? Очевидна відповідь — «уся картка». Загорнути весь контент картки в один тег <a> — цілком реально.

<li>  
    <a href="/card-design-woes">
        <img src="/path/to/image.png" alt="">
        <h2>Card design woes</h2>
        <p>Ten common pitfalls to avoid when designing card components.</p>
        <small>By Heydon Pickering</small>
    </a>
</li>  

Таке рішення не безпроблемне. Тепер увесь контент картки — вміст посилання. І коли скрінрідер до нього добереться, це прозвучить приблизно так: «Card design woes, ten common pitfalls to avoid when designing card components, by Heydon Pickering, link».

З погляду сприйняття, такий варіант допустимий, але перевантажений — особливо якщо контенту стане більше і він буде інтерактивним. Окрім того, незвично бачити блоковий елемент <h2> всередині рядкового <a>, хоча технічно HTML5 це дозволяє.

Якщо ви почнете додавати інтерактивність — наприклад, зробите ім'я автора посиланням, усе стане ще більш заплутаним. Деякі скрінрідери прочитають лише перший елемент «блокового посилання», обрізаючи контент, а з ним — доступ до додаткової функціональності. Зазвичай ви не чекаєте, що всередині посилання буде ще одне посилання, і незрячий користувач може також його «проскочити».

<li>  
    <a href="/card-design-woes">
        <img src="/path/to/image.png" alt="">
        <h2>Card design woes</h2>
        <p>Ten common pitfalls to avoid when designing card components.</p>
        <small>By <a href="/author/heydon">Heydon Pickering</a></small>
    </a>
</li>  

Багато викликів інклюзивного дизайну виходять із конфлікту між логічною структурою документа, красивим візуальним оформленням та інтуїтивною взаємодією. Якщо ми чимось жертвуємо — досвід користування буде біднішим. Компромісів не уникнути, але це мусять бути справедливі компроміси.

Тож найкращий підхід — почати з надійної HTML-структури, далі — використовувати CSS для стилізації, а JavaScript — для покращення поведінки там, де це виправдано. У цій простій картці заголовок — назва статті, для якої картка слугує тізером. Тоді логічно, що цей текст буде основним посиланням.

<li>  
    <img src="/path/to/image.png" alt="">
    <h2>
        <a href="/card-design-woes">Card design woes</a>
    </h2>
    <p>Ten common pitfalls to avoid when designing card components.</p>
    <small>By Heydon Pickering</small>
</li>  

Перевага такого методу над кнопкою «читати більше» в тому, що кожне посилання має унікальне опиcове ім'я, що корисно, коли користувачі переглядають зібрані списки посилань. Для прикладу, комбінація клавіш Insert + F7 у NVDA дає користувачеві доступ до всіх посилань на сторінці.

Проте мені б хотілося, щоби сама картка була клікабельною. Через відсутність явної кнопки «читати більше» у моєму випадку не зрозуміло, куди клікати, тому «всюди» має усунути проблему. Також тоді буде простіше поцілити в посилання мишкою чи дотиком.

Є кілька способів це зробити.

Трюк із псевдоконтентом

Якщо виконати ці кроки, можна зробити клікабельною всю картку, не змінюючи при цьому встановлену розмітку:

  1. Контейнеру картки задайте position: relative.
  2. Для посилання <a> створіть псевдоконтент ::after і задайте йому position: absolute.
  3. Прослідкуйте, щоби кожен псевдоконтент ::after для посилання мав нульове значення для left, top, right і bottom.

Це розтягує вміст посилання на всю картку, роблячи її клікабельною, як кнопку.

З одного боку, це розумне рішення, тому що воно не покладається на JavaScript (і навіщо використовувати JavaScript на статичному сайті, якщо можна його уникнути?). З іншого боку, воно не ідеальне, тому що тепер складно виділити текст всередині картки (посилання спрацьовує як маска поверх картки). Ви можете вибірково «підняти» елементи, задавши їм position: relative, але тоді їхня розмітка буде випадати із повністю клікабельної ділянки.

Інклюзивні компоненти: картки

Демо трюку із псевдоконтентом

Надлишкова клік-подія

Є альтернативний спосіб — ми не відмовляємося від JavaScript і використовуємо контейнер картки як проксі для посилання. Обробник подій для елемента контейнера картки просто запустить метод кліка для посилання всередині нього. Це ніяк не вплине на користувача, котрий працює з клавіатури й для якого оригінальне посилання залишається всередині контенту.

card.addEventListener(() => link.click());

Технічно, через спливання події, якщо я прямо клікну на посилання (роблячи його ціллю події) подія спрацює двічі. Навіть якщо жодного побічного ефекту від цього під час тестування не виявлено, ви можете уникнути цього в такий спосіб:

card.addEventListener(e => {  
    if (link !== e.target) {
        link.click()
    }
});

Тепер користувач зможе виділити текст, але при цьому спрацює клік-подія і можна буде перейти за посиланням. Потрібно визначити, скільки часу в користувача триває період між mousedown і mouseup, щоби можна було уникнути події тоді, коли користувач хоче лише виділити текст.

Ось повний скрипт, використаний в демо. Я виявив, що період займає близько 200 мілісекунд. У будь-якому випадку незвично затягнутий клік спрацює із другої спроби.

const cards = document.querySelectorAll('.card');  
Array.prototype.forEach.call(cards, card => {  
    let down, up, link = card.querySelector('h2 a');
    card.onmousedown = () => down = +new Date();
    card.onmouseup = () => {
        up = +new Date();
        if ((up - down) < 200) {
            link.click();
        }
    }
});   

Демо рішення із надлишковою клік-подією

Малоймовірно, що користувач захоче виділити текст в картці чи тізері, маючи доступ до повного вмісту картки, який вона відкриває. Проте його може здивувати, якщо текст не виділяється зовсім.

Доступність

Якщо картка повністю інтерактивна, користувач має про це знати. Ми повинні забезпечити йому візуальні підказки.

У трюка з псевдоконтентом вся картка має стиль курсора pointer, оскільки містить посилання, яке її перекриває. Для рішення із резервною клік-подією його треба додати вручну за допомогою JavaScript, бо якщо JavaScript не спрацює, стиль не відповідатиме дійсності.

card.style.cursor = 'pointer';

На додаток до заокругленого, кнопкоподібного дизайну моїх карток, стиль наведення курсору прояснює сприйняття. Мені подобається користуватися box-shadow, тому що — на відміну від outline — він «поважає» заокруглені кути.

.card:hover {
    box-shadow: 0 0 0 0 0.25rem;
}
Інклюзивні компоненти: картки

Коли у картці є стилі для ховера, які також мають бути стилями для фокуса, нам відкривається ще одна цікава проблема. Використання :focus застосовується лише для стилів самого посилання. Це незначне питання, однак було б добре, якби зрячі користувачі клавіатури побачили той красивий та великий, відповідний до розміру картки, стиль, який бачать користувачі мишки. На щастя, це можливо з використанням :focus-within:

.card a:focus {
    text-decoration: underline;
}

.card:focus-within {
    box-shadow: 0 0 0 0.25rem;
}

.card:focus-within a:focus {
    text-decoration: none;
}

Тут я прогресивно вдосконалив стилі для фокуса, використовуючи каскад CSS. Спочатку я застосував базовий стиль для фокуса — для самого посиланню. Тоді я використав :focus-within, який би відповідав :hover-стилям. Нарешті, я видалив непотрібний базовий :focus-стиль лише там, де :focus-within підтримується. Ідеться про те, що коли :focus-within не підтримується, другий і останній блоки коду будуть ігноруватися. Суть полягає в тому, що користувачі старіших браузерів, які не підтримують :focus-within, зможуть все-таки побачити запасний стиль для фокуса.

Інклюзивні компоненти: картки

Увага! Не застосовуйте :hover-стиль для того самого блоку, що й :focus-within. Інакше цілий блок буде проігноровано там, де :focus-within не підтримується, і разом з ним ви втратите весь стиль для ховера.

Толерування контенту

Одним з аспектів інклюзивного дизайну, про який часто забувають, є мистецтво розробки інтерфейсів, що будуть толерувати різні рівні контенту. Якщо інтерфейс «ламається» через надмір або брак контенту, ми обмежуємо те, що хотіли сказати його автори.

Наші картки повинні показувати заголовки та опис різної довжини так, щоби при цьому не псувався ні сам дизайн, ні його сприйняття. Спершу я додаю пару контейнерів <div> для зручності (.img і .text):

<li class="card">  
    <div class="img">
        <img src="/path/to/image.png" alt="">
    </div>
    <div class="text">
        <h2>
            <a href="/card-design-woes">Card design woes</a>
        </h2>
        <p>Ten common pitfalls to avoid when designing card components.</p>
        <small>By Heydon Pickering</small>
    </div>
</li>  

Тоді обидва контейнери: .card і .text (який всередині нього) я поміщаю в контекст Flexbox, використовуючи flex-direction: column.

.card, .card .text {
  display: flex;
  flex-direction: column;
}

Далі я змушую всі текстові елементи зайняти весь доступний простір за допомогою flex-grow: 1:

.card .text {
  flex-grow: 1;
}

Нарешті, щоби трохи їх збалансувати, я беру останній текстовий елемент і задаю йому верхній марджін auto:

.card .text :last-child {
  margin-top: auto;
}

Це штовхає елемент атрибуції (підпис) у демо донизу картки, незалежно від її висоти.

Інклюзивні компоненти: картки

Лишилося тільки додати нижній марджін другому з кінця елементу, щоби впевнитися у мінімальному відступі. Для цього я використовую owl selector, який додає звичайний марджін. Все це разом матиме такий вигляд:

.card, .card .text {
  display: flex;
  flex-direction: column;
}

.card .text {
  flex-grow: 1;
}

.card .text > * + * {
  margin-top: 0.75rem;
}

.card .text :last-child {
  margin-top: auto;
}

.card .text :nth-last-child(2) {
  margin-bottom: 0.75rem;
}

Зверніть увагу, що ми тут використовуємо марджіни в алгоритмічний спосіб, використовуючи позицію і контекст, а не конкретні властивості елемента. Неважливо, який елемент ми розмістимо в текстовому контейнері, ефект буде той самий.

ПРОГРЕСИВНЕ ПОКРАЩЕННЯ З ГРІДАМИ

Тепер потрібно лише помістити картки в контекст CSS Flexbox або CSS grid, де картки будуть розтягуватися на однакову висоту — по висоті картки з найбільшою кількістю контенту.

Цього ефекту можна досягнути і за допомогою Grid, і за допомогою Flexbox, однак я надаю перевагу грідівському алгоритму обтікання; grid-gap — це найпростіший спосіб розміщення карток без необхідності використовувати метод із мінусовим марджіном.

За допомогою @supports я можу реалізувати просту, одноколонкову розмітку, а потім покращити її грідами там, де вони підтримуються:

.card .text {
  max-width: 60ch;
}

.card + .card {
    margin-top: 1.5rem;
}

@supports (display: grid) {
    .cards > ul {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
        grid-gap: 1.5rem;
    }

    .card + .card {
        margin-top: 0;
    }
}

Зверніть увагу на max-width, який я задаю текстовому контейнеру: приблизно 60 символів. Це унеможливлює розтягнення рядків на великих екранах там, де гріди не підтримуються. Звичайно, за таких умов картки не будуть схожі на, власне, картки, але, принаймні, вони будуть читабельні.

Інклюзивні компоненти: картки

На щастя, зараз підтримка грідів досить широка.

РОЗМІРИ ЗОБРАЖЕННЯ

Окрім роботи над гнучким (flexible) текстовим контентом, ми також маємо працювати з різними розмірами завантаженого зображення. Заданий object-fit: cover у поєднанні з width і height на 100% спрощує задачу. Це дозволяє нам відрегулювати висоту контейнера для зображення на свій смак, не показуючи при цьому прогалин і не деформуючи зображення.

.card .img {
  height: 5rem;
}

.card .img img {
  object-fit: cover;
  height: 100%;
  width: 100%;
}
Інклюзивні компоненти: картки
Легкий нахил, застосований до зображення, досягається за допомогою clip-path. Це теж прогресивне покращення. При цьому контент не приховується у випадку, коли clip-path не підтримується.

У використанні object-fit:cover є характерний компроміс: для підтримки правильного співвідношення сторін, зображення стане обрізаним уздовж двох або більше країв. Оскільки саме зображення завжди відцентроване в рамках свого контейнера, це допомагає підібрати картинки, у яких центр відфокусований. Налаштування фокусу можна регулювати за допомогою object-position.

Авторське посилання

Що ж до авторського посилання, то найпершим врахуйте — чи потрібно або бажано зробити це посилання інтерактивним у межах картки. Особливо, якщо сторінка автора прив'язана до постійного посилання (permalink), на яку вказує картка. Додайте перемикання табами лише там, де це потрібно, оскільки зловживаннями табами уповільнюють та ускладнюють навігацію з клавіатури.

Заради справедливості скажу, що існує сценарій використання прив'язки автора всередині картки. Це можна реалізувати за допомогою описаних вище трюків із псевдоконтентом та методів JavaScript. Декларація position:relative підніме посилання над псевдоконтентом у першому прикладі. Всупереч поширеній думці, потрібне лише позиціювання, а не z-index, оскільки авторське посилання стоїть після первинного посилання в джерелі.

.card small a {
    position: relative;
}

Демо авторського посилання (з використанням псевдоконтенту)

Для деяких користувачів проблемою буде влучити на авторське посилання. У більшості сценаріїв пара неточних клацань чи натискань відносно нешкідлива, але там, де потрібне посилання розміщене над іншим інтерактивним елементом, інший елемент може активізуватися. Тоді це означатиме завантаження неправильної сторінки.

Отже, чому б не збільшити «зону натискання» авторського посилання, щоби пом'якшити це? Ми можемо використовувати паддінги. Лівий паддінг залишається незадієним, оскільки це відсуне посилання від решти тексту.

.card small a {
  position: relative;
  padding: 0.5rem 0.5rem 0.5rem 0;
}
Інклюзивні компоненти: картки

Багато хто має труднощі із наведенням на елементи дотиком, зокрема це люди з хворобою Паркінсона чи ревматизмом.

Також доцільно збільшити відстань між одноколонними картками на маленьких екранах, іншими словами, так простіше уникнути активації карти під час прокрутки.

@media (max-width: 400px) {
  .cards > ul {
    grid-gap: 4.5rem;
  }
}

Заклики до дії

Як я зазначав раніше, численні посилання «читати більше» не приносять користі, якщо вони вирвані з контексту та об'єднані в глосарій. Краще цього уникати. Однак явний заклик до дії може виявитись корисним. Без нього користувачі можуть не усвідомлювати, що картки є інтерактивними. Бути очевидним, як правило, найкращий підхід в дизайні інтерфейсів.

То як нам реалізувати ці кнопки й зберегти при цьому описовий текст посилання? Одна з можливостей — зберегти тайтл/заголовок як основне посилання, а декоративну кнопку «читати більше» додати окремо.

<li>  
    <img src="/path/to/image.png" alt="">
    <h2>
        <a href="/card-design-woes">Card design woes</a>
    </h2>
    <p>Ten common pitfalls to avoid when designing card components.</p>
    <span class="cta" aria-hidden="true">read more </span>
    <small>By Heydon Pickering</small>
</li> 

Якби кнопка була б ще й активним посиланням, з'явилась би непотрібна функціональнісь і додаткова зупинка для таба. Натомість «кнопка» призначена лише для показу, вона прихована від допоміжних технологій за допомогою aria-hidden. Хитрість полягає в тому, щоби на екрані з'явилася кнопка як інтерактивний елемент: інша робота для :focus-within.

Я шукаю фокус усередині <h2> і використовую загальний комбінатор суміжних селекторів, щоби делегувати стиль фокусування кнопці заклику до дії.

.card h2 a:focus {
    text-decoration: underline;
}

.card h2:focus-within ~ .cta {
    box-shadow: 0 0 0 0.125rem;
}

.card:focus-within h2 a:focus {
    text-decoration: none;
}

Візуально порядок фокусування залишається логічним — від тайтлу (кнопка заклику до дії) до авторського посилання:

Інклюзивні компоненти: картки

Для зрячих користувачів скрінрідера тут можлива невелика плутанина, оскільки «читати більше» не прозвучить, хоча на цьому елемент хочеться сфокусуватись. На щастя, ми можемо приєднати «читати більше» до посилання як опис, використовуючи aria-describedby. Тепер користувачі почують: «Card design woes, link, read more». Опис завжди читається останнім.

<li>  
    <img src="/path/to/image.png" alt="">
    <h2>
        <a href="/card-design-woes" aria-describedby="desc-card-design-woes">Card design woes</a>
    </h2>
    <p>Ten common pitfalls to avoid when designing card components.</p>
    <span class="cta" aria-hidden="true" id="desc-card-design-woes">read more</span>
    <small>By Heydon Pickering</small>
</li>  

Це працює, тому що навіть там, де застосовано aria-hidden="true", утворений зв'язок залишається непорушним, а опис — доступним для посилання. Це корисно, коли ви плануєте використовувати елемент для опису, але не хочете, щоби допоміжні технології безпосередньо розпізнавали цей елемент. Користувачеві скрінрідера було б незрозуміла ситуація, коли можна навести на кнопку заклику до дії і почути імператив «читати більше», хоча сам елемент при цьому не відіграє жодної ролі або не є інтерактивним.

Зверніть увагу, що оскільки заклик до дії в кожному випадку каже «читати більше», кожне посилання на картку повинне посилатися лише на один із елементів заклику до дії. Проте в шаблонному циклі, це, вірогідно, буде важко реалізувати.

Демо закликів до дії


Унікальні рядки

При створенні динамічного контенту, перебираючи дані, ми стикаємося з речами, які неможливо реалізувати. Наприклад, задати вручну значення id.

Для побудови робочих зв'язків між id (подібно до aria-describedby прив'язки із поперднього прикладу) ці id номери повинні бути послідовними та унікальними. Є декілька способів це зробити.

Перший: створити унікальний рядок за допомогою якоїсь псевдорендомізації. Ось лаконічне рішення, що базується на git-репозиторії Ґордана Брандера:

const uniq = Math.random().toString(36).substr(2, 9);

На Vue.js я можу згенерувати цей унікальний рядок у такий спосіб:

<!-- the id attribute -->

:id="'desc-' + uniq"

<!-- the aria-describedby attribute -->

:aria-describedby="'desc-' + uniq"

Одним недоліком такого підходу є те, що тестування знімками не зможе синхронізуватися, оскільки при кожному білді буде генеруватися новий унікальний рядок. З цієї причини я вважаю за краще використовувати просту функцію «slugify», що базується на рядку, який вже є на сторінці. У такому випадку тайтл для картки видається доречним.

Ось ця функція у вигляді невеликої утиліти, яка конвертує «My card component!» у my-card-component:

export default text => {
  return text
    .toString()
    .toLowerCase()
    .replace(/\\s+/g, '-')
    .replace(/[^\\w\\-]+/g, '')
    .replace(/\\-\\-+/g, '-')
    .replace(/^-+/, '')
    .replace(/-+$/, '');
};

Альтернативний текст

Досі я припускав, що зображення картки — декоративне і не потребує альтернативного тексту, звідси alt="". Якби ми пропустили цей порожній alt, скрінрідери ідентифікували б зображення і прочитали б (частину) src атрибута як фолбек, чого б нам зовсім не хотілося.

Якби зображення мало значення з точки зору змісту (наприклад, зовнішній вигляд товару на продаж), ми, звичайно, повинні були б вказати відповідне значення alt. Але у нас є проблема, яку передбачив Енді Кірк, котрий зв'язався зі мною з цього приводу на Patreon.

Зараз зображення з'являється перед текстом. Оскільки заголовки, у нашому випадку <h2>, представляють секції, зображення перед заголовком говорить про те, що воно не належить до секції. З одного боку, <li> групує зображення з текстом, але не всі користувачі сприймуть цю структуру так. Коли користувач скрінрідера, що працює з NVDA, натискає 2, щоби перейти до наступного <h2>, він «перескочить» зображення і пропустить його.

Інклюзивні компоненти: картки

ПАРАМЕТР ORDER

На щастя, параметр order у Flexbox дозволяє нам змінювати початковий порядок елементів, і за тим корегувати їх візуальну презентацію.

Спочатку я міняю місцями контейнери для зображення і тексту...

<li class="card">  
    <div class="text">
        <h2>
            <a href="/card-design-woes">A great product</a>
        </h2>
        <p>Description of the great product</p>
        <small>By Great Products(TM)</small>
    </div>
    <div class="img">
        <img src="/path/to/image.png" alt="Description of the great product's appearance">
    </div>
</li>  

... тоді я просто переміщаю контейнер зображення поверх макета:

.card .text {
  order: 1;
}

Маніпулювання порядком елементів за допомогою CSS може спричинити проблеми з доступністю, особливо там, де це призводить до конфлікту між порядком фокусування і візуальним макетом. Це може спантеличити.

У нашому випадку порядок фокусування не застосовується, оскільки зображення не фокусується. Для зрячих користувачів скрінрідерів це видасться дивним, але навряд чи це спричинить серйозні проблеми зі сприйняттям, оскільки кожна картка є візуально самостійною.

Скрінрідери, як от VoiceOver, забезпечують видиме кільце, подібне на стиль фокусування, для кожного елемента, який переглядає користувач (включаючи елементи, що не фокусуються). Це кільце показує користувачам, на якому елементі вони перебувають, де б вони не були.

Підсумки

Деякі з розглянутих тут ідей та методів можуть не підійти для ваших карток; інші спрацюють. Не мені вказувати, як ви повинні реалізувати «картку», тому що я не знаю ваших вимог. Але сподіваюся, що я дав вам кілька ідей для розв'язування можливих проблем та підказав, як покращити інтерфейс методами, розрахованими на широке коло користувачів.

ЧЕКЛІСТ

  • Використовуйте розмітку списків для групування карток.
  • Переконайтесь, що ваші картки не «розлізуться», коли текст або зображення не відповідають певним вимогам щодо співвідношення сторін.
  • Уникайте надмірної функціональності та зменшуйте перемикання табами. Картки — це не мініатюрні вебсторінки.
  • Пам'ятайте, що заголовки відкривають секцію. Більшість із того, що належить до секції, має слідувати за заголовком в джерелі.
Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 4.8K
Приєднався: 10 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація