mirror of https://github.com/gtitov/wcwb.git
parent
9a54c78f0b
commit
53c1d0e0e1
@ -0,0 +1,236 @@
|
||||
<multiple-choice
|
||||
data-correct-label="Верно 😸"
|
||||
data-incorrect-label="Надо поразмыслить 🐱"
|
||||
>
|
||||
<form onsubmit="event.preventDefault()">
|
||||
<ol class="opt-list not-content"><slot /></ol>
|
||||
<div class="footer">
|
||||
<button class="submit" type="submit" disabled> Проверить </button>
|
||||
<div class="answer sr-only" role="alert"></div>
|
||||
</div>
|
||||
</form>
|
||||
</multiple-choice>
|
||||
|
||||
<style>
|
||||
multiple-choice {
|
||||
display: block;
|
||||
padding-bottom: 1rem;
|
||||
color: var(--sl-color-text-accent);
|
||||
}
|
||||
|
||||
form > * + * {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.opt-list {
|
||||
list-style: none;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.submit,
|
||||
.answer {
|
||||
padding: 0.8rem;
|
||||
border-radius: 1.5rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.submit {
|
||||
cursor: pointer;
|
||||
color: var(--sl-color-accent);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.submit:focus {
|
||||
background-color: hsl(224, 20%, 94%);
|
||||
outline: 3px solid var(--sl-color-accent);
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
.submit:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.submit:hover {
|
||||
background: var(--sl-color-text-accent);
|
||||
color: var(--sl-color-black);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.submit:disabled,
|
||||
.submit:active {
|
||||
transform: translateY(0.25rem);
|
||||
}
|
||||
|
||||
.submit:disabled {
|
||||
background-color: hsl(224, 20%, 94%);
|
||||
color: hsl(224, 7%, 36%);
|
||||
opacity: 0.65;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.answer {
|
||||
text-align: center;
|
||||
transition-property: color, background-color;
|
||||
transition-duration: 250ms;
|
||||
transition-timing-function: ease-out;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.correct {
|
||||
color: var(--sl-color-green-low);
|
||||
background-color: var(--sl-color-green);
|
||||
animation: tada 1s 0;
|
||||
}
|
||||
|
||||
.wrong {
|
||||
color: var(--sl-color-red-low);
|
||||
background-color: var(--sl-color-red);
|
||||
animation: nope 0.3s 0;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.submit {
|
||||
transition-property: box-shadow, transform;
|
||||
transition-duration: 0.15s;
|
||||
transition-timing-function: cubic-bezier(0.4, 2.5, 0.6, 1);
|
||||
}
|
||||
|
||||
.correct,
|
||||
.wrong {
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tada {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
10%,
|
||||
20% {
|
||||
transform: scale3d(0.8, 0.8, 0.8) rotate3d(0, 0, 1, -3deg);
|
||||
}
|
||||
30%,
|
||||
50%,
|
||||
70%,
|
||||
90% {
|
||||
transform: scale3d(1.05, 1.05, 1.05) rotate3d(0, 0, 1, 3deg);
|
||||
}
|
||||
40%,
|
||||
60%,
|
||||
80% {
|
||||
transform: scale3d(1.05, 1.05, 1.05) rotate3d(0, 0, 1, -3deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes nope {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
20%,
|
||||
60% {
|
||||
transform: translate3d(-0.5rem, 0, 0);
|
||||
}
|
||||
40%,
|
||||
80% {
|
||||
transform: translate3d(0.5rem, 0, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
class MultipleChoice extends HTMLElement {
|
||||
#defaultCorrectLabel: string;
|
||||
#defaultIncorrectLabel: string;
|
||||
#key: string;
|
||||
#submitEl: HTMLButtonElement;
|
||||
#answerEl: HTMLParagraphElement;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.#defaultCorrectLabel = this.dataset.correctLabel!;
|
||||
this.#defaultIncorrectLabel = this.dataset.incorrectLabel!;
|
||||
this.#key = Math.random().toString();
|
||||
this.#submitEl = this.querySelector(".submit")!;
|
||||
this.#answerEl = this.querySelector(".answer")!;
|
||||
this.querySelectorAll(".opt-list > li").forEach((li) =>
|
||||
this.#upgradeListItem(li)
|
||||
);
|
||||
}
|
||||
|
||||
#upgradeListItem(li: Element) {
|
||||
const option = li.querySelector(
|
||||
'input[type="radio"]'
|
||||
) as HTMLInputElement | null;
|
||||
if (!option) return;
|
||||
|
||||
option.removeAttribute("disabled");
|
||||
option.setAttribute("name", this.#key);
|
||||
option.addEventListener("change", () => {
|
||||
this.#clearAnswer();
|
||||
this.#enableSubmit();
|
||||
});
|
||||
if (option.checked) this.#enableSubmit();
|
||||
}
|
||||
|
||||
/** Clear the answer text and hide its container visually. */
|
||||
#clearAnswer() {
|
||||
this.#answerEl.innerText = "";
|
||||
this.#answerEl.classList.remove("wrong", "correct");
|
||||
this.#answerEl.classList.add("sr-only");
|
||||
}
|
||||
|
||||
/** Show the answer result to the user. */
|
||||
#setAnswer(isCorrect: boolean) {
|
||||
const answerTemplate = this.querySelector(
|
||||
"input:checked ~ template"
|
||||
) as HTMLTemplateElement | null;
|
||||
if (answerTemplate) {
|
||||
this.#answerEl.replaceChildren(answerTemplate.content.cloneNode(true));
|
||||
} else {
|
||||
this.#answerEl.innerText = isCorrect
|
||||
? this.#defaultCorrectLabel
|
||||
: this.#defaultIncorrectLabel;
|
||||
}
|
||||
this.#answerEl.classList.remove("sr-only", "wrong", "correct");
|
||||
this.#answerEl.classList.add(isCorrect ? "correct" : "wrong");
|
||||
}
|
||||
|
||||
/** Activate the submit button, preparing it to evaluate the form when clicked. */
|
||||
#enableSubmit() {
|
||||
this.#submitEl.removeAttribute("disabled");
|
||||
this.#submitEl.classList.remove("sr-only");
|
||||
this.#submitEl.onclick = () => this.#submitAnswer();
|
||||
}
|
||||
|
||||
/** Disable the submit button and hide it visually. */
|
||||
#disableSubmit() {
|
||||
this.#submitEl.setAttribute("disabled", "");
|
||||
this.#submitEl.classList.add("sr-only");
|
||||
this.#submitEl.onclick = null;
|
||||
}
|
||||
|
||||
/** Check if the selected option is correct and display the result. */
|
||||
#submitAnswer() {
|
||||
const selection = this.querySelector(
|
||||
"input:checked"
|
||||
) as HTMLInputElement | null;
|
||||
if (!selection) return;
|
||||
|
||||
this.#disableSubmit();
|
||||
const isCorrect =
|
||||
selection.dataset.isCorrect !== undefined &&
|
||||
["", "true"].includes(selection.dataset.isCorrect);
|
||||
this.#setAnswer(isCorrect);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("multiple-choice", MultipleChoice);
|
||||
</script>
|
||||
@ -0,0 +1,94 @@
|
||||
---
|
||||
export interface Props {
|
||||
isCorrect?: boolean;
|
||||
}
|
||||
|
||||
const { isCorrect } = Astro.props as Props;
|
||||
---
|
||||
|
||||
<li>
|
||||
<label>
|
||||
<input type="radio" disabled data-is-correct={isCorrect} /><span
|
||||
><slot /></span
|
||||
>
|
||||
{
|
||||
Astro.slots.has("feedback") && (
|
||||
<template>
|
||||
<slot name="feedback" />
|
||||
</template>
|
||||
)
|
||||
}
|
||||
</label>
|
||||
</li>
|
||||
|
||||
<style>
|
||||
label {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
border-radius: 1rem;
|
||||
margin-inline: -0.5rem;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
label:hover {
|
||||
background-color: var(--sl-color-accent-low);
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="radio"] ~ * {
|
||||
color: var(--sl-color-text-accent);
|
||||
}
|
||||
|
||||
input[type="radio"]:checked ~ * {
|
||||
color: var(--sl-color-white);
|
||||
}
|
||||
|
||||
input[type="radio"]:focus::after {
|
||||
outline: 3px solid var(--sl-color-text-accent);
|
||||
outline-offset: 0.5rem;
|
||||
}
|
||||
|
||||
input[type="radio"]:focus,
|
||||
input[type="radio"]:not(:focus-visible)::after {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type="radio"]::after {
|
||||
display: block;
|
||||
content: "";
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
font-size: 1.25em;
|
||||
font-weight: 900;
|
||||
border: 2px solid var(--sl-color-accent-high);
|
||||
border-radius: 100%;
|
||||
color: var(--sl-color-white);
|
||||
box-shadow: -3px 3px var(--theme-shade-subtle);
|
||||
}
|
||||
|
||||
input[type="radio"]:checked::after {
|
||||
color: hsl(var(--color-base-white), 100%);
|
||||
background-image: radial-gradient(
|
||||
var(--sl-color-white) 50%,
|
||||
transparent 0%,
|
||||
transparent
|
||||
);
|
||||
border-color: var(--sl-color-white);
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
input[type="radio"]:checked::after {
|
||||
background-color: Highlight;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in new issue