From c7a6f07b362ec2320a7af8b443a7c4198af8b599 Mon Sep 17 00:00:00 2001 From: dorayx Date: Wed, 21 Feb 2024 22:04:58 +0100 Subject: [PATCH] feat(challenge): initial 02-progress-steps --- README.md | 1 + challenges/02-progress-steps/README.md | 23 +++ challenges/02-progress-steps/docs/.gitkeep | 0 .../02-progress-steps/docs/snapshot.png | Bin 0 -> 11069 bytes challenges/02-progress-steps/index.html | 13 ++ challenges/02-progress-steps/package.json | 11 ++ challenges/02-progress-steps/public/vite.svg | 1 + challenges/02-progress-steps/src/main.ts | 33 ++++ .../src/progress-steps.module.ts | 136 +++++++++++++ challenges/02-progress-steps/src/style.css | 179 ++++++++++++++++++ .../02-progress-steps/src/vite-env.d.ts | 1 + challenges/02-progress-steps/tsconfig.json | 32 ++++ .../02-progress-steps/tsconfig.node.json | 15 ++ challenges/02-progress-steps/vite.config.ts | 19 ++ yarn.lock | 6 + 15 files changed, 470 insertions(+) create mode 100644 challenges/02-progress-steps/README.md create mode 100644 challenges/02-progress-steps/docs/.gitkeep create mode 100644 challenges/02-progress-steps/docs/snapshot.png create mode 100644 challenges/02-progress-steps/index.html create mode 100644 challenges/02-progress-steps/package.json create mode 100644 challenges/02-progress-steps/public/vite.svg create mode 100644 challenges/02-progress-steps/src/main.ts create mode 100644 challenges/02-progress-steps/src/progress-steps.module.ts create mode 100644 challenges/02-progress-steps/src/style.css create mode 100644 challenges/02-progress-steps/src/vite-env.d.ts create mode 100644 challenges/02-progress-steps/tsconfig.json create mode 100644 challenges/02-progress-steps/tsconfig.node.json create mode 100644 challenges/02-progress-steps/vite.config.ts diff --git a/README.md b/README.md index 7d2c52f..8d415db 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,4 @@ A series of front-end challenges to experiment and demonstrate my ideas and thou | # | Title | Experiment | Tech Stack | Source | Live Demo | | --- | --------------- | --------------------------- | ------------ | ---------------------------------------------------------------- | --------- | | 01 | Expanding Cards | CSS Variables as Parameters | 🍦Vanilla TS | [challenges/01-expanding-cards](./challenges/01-expanding-cards) | 🚧 | +| 02 | Progress Steps | A11y for Progress Steps | 🍦Vanilla TS | [challenges/02-progress-steps](./challenges/02-progress-steps) | 🚧 | diff --git a/challenges/02-progress-steps/README.md b/challenges/02-progress-steps/README.md new file mode 100644 index 0000000..7a2d456 --- /dev/null +++ b/challenges/02-progress-steps/README.md @@ -0,0 +1,23 @@ +# 02 Progress Steps + +
+ +## Requirements + +- [x] Create a progress bar with 4 steps. +- [x] When a prev or next button is clicked, the progress bar should move to the next or previous step. +- [x] Accessibility Support (Keyboard Navigation). + +## Considerations + +- Use `role="group"` to group the steps. +- Use `aria-labelledby` to describe the button text for screen readers. +- Use `aria-current` to indicate the current step. +- Use `aria-live` to announce the current step when the user navigates through the buttons. + +## Reference + +- https://www.udemy.com/course/50-projects-50-days/learn/lecture/23595222#overview +- https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/group_role +- https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions +- https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current diff --git a/challenges/02-progress-steps/docs/.gitkeep b/challenges/02-progress-steps/docs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/challenges/02-progress-steps/docs/snapshot.png b/challenges/02-progress-steps/docs/snapshot.png new file mode 100644 index 0000000000000000000000000000000000000000..1e7b83bc05bccd300e73d6dea262dec8e68792c0 GIT binary patch literal 11069 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 + + + + + + #02 Progress Steps + + +
+ + + diff --git a/challenges/02-progress-steps/package.json b/challenges/02-progress-steps/package.json new file mode 100644 index 0000000..239c4d4 --- /dev/null +++ b/challenges/02-progress-steps/package.json @@ -0,0 +1,11 @@ +{ + "name": "02-progress-steps", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + } +} diff --git a/challenges/02-progress-steps/public/vite.svg b/challenges/02-progress-steps/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/challenges/02-progress-steps/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/challenges/02-progress-steps/src/main.ts b/challenges/02-progress-steps/src/main.ts new file mode 100644 index 0000000..67d3cc2 --- /dev/null +++ b/challenges/02-progress-steps/src/main.ts @@ -0,0 +1,33 @@ +import './style.css'; +import { ProgressStepsModule } from '@/progress-steps.module.ts'; + +document.querySelector('#app')!.innerHTML = ` +
+
    +
  1. Step1
  2. +
  3. Step2
  4. +
  5. Step3
  6. +
  7. Step4
  8. +
+
+ + +
+
+`; + +document.addEventListener('DOMContentLoaded', () => { + const root = document.querySelector('.js-progress') as HTMLElement; + if (!root) { + throw new Error('Root element not found'); + } + + ProgressStepsModule.init(root); +}); diff --git a/challenges/02-progress-steps/src/progress-steps.module.ts b/challenges/02-progress-steps/src/progress-steps.module.ts new file mode 100644 index 0000000..de2d1c7 --- /dev/null +++ b/challenges/02-progress-steps/src/progress-steps.module.ts @@ -0,0 +1,136 @@ +export class ProgressStepsModule { + static init(root: HTMLElement) { + return new ProgressStepsModule(root); + } + + private readonly elSteps: NodeListOf; + private readonly elButtons: [HTMLButtonElement, HTMLButtonElement]; + private readonly elButtonLabelSlots: [HTMLElement, HTMLElement]; + private destroyHandlers: (() => void)[] = []; + + private currentStep = 0; + + private constructor(private root: HTMLElement) { + this.elSteps = this.root.querySelectorAll('.progress__indicator__circle'); + + this.elButtons = [ + this.root.querySelector('[data-btn-prev]')!, + this.root.querySelector('[data-btn-next]')!, + ]; + + this.elButtonLabelSlots = this.elButtons.map((el) => el.querySelector('[data-slot]')!) as [ + HTMLElement, + HTMLElement, + ]; + + this.root.style.setProperty('--steps-count', String(this.elSteps.length)); + + this.setDefaultStep(); + this.handleButtonsClick(); + this.handleButtonsKeydown(); + } + + public destroy() { + this.destroyHandlers.forEach((handler) => handler()); + } + + private setDefaultStep() { + this.setActiveStep(this.currentStep); + } + + private handleButtonsClick = () => { + const handler = (event: MouseEvent) => { + const target = event.target as HTMLElement; + if (target.dataset.btnPrev !== undefined) { + this.movePrev(); + } else if (target.dataset.btnNext !== undefined) { + this.moveNext(); + } + }; + + this.root.addEventListener('click', handler); + this.destroyHandlers.push(() => this.root.removeEventListener('click', handler)); + }; + + private handleButtonsKeydown = () => { + const handler = (event: KeyboardEvent) => { + if (event.key !== 'Enter') { + return; + } + + const target = event.target as HTMLElement; + if (target.dataset.btnPrev !== undefined) { + this.movePrev(); + } else if (target.dataset.btnNext !== undefined) { + this.moveNext(); + } + }; + + this.root.addEventListener('keydown', handler); + this.destroyHandlers.push(() => this.root.removeEventListener('keydown', handler)); + }; + + private moveNext() { + if (this.currentStep < this.elSteps.length) { + this.currentStep++; + this.setActiveStep(this.currentStep); + } + } + + private movePrev() { + if (this.currentStep > 0) { + this.currentStep--; + this.setActiveStep(this.currentStep); + } + } + + private setActiveStep(step: number) { + this.root.style.setProperty('--current-step', String(step)); + + this.elSteps.forEach((el, index) => { + el.removeAttribute('aria-current'); + el.removeAttribute('aria-live'); + el.removeAttribute('aria-atomic'); + + if (index <= step) { + el.setAttribute('data-active', 'true'); + } else { + el.removeAttribute('data-active'); + } + }); + + const targetStep = this.elSteps.item(step); + targetStep?.setAttribute('aria-current', 'step'); + targetStep?.setAttribute('aria-live', 'polite'); + targetStep?.setAttribute('aria-atomic', 'true'); + + this.setButtonsState(step); + this.setButtonLabels(step); + } + + private setButtonsState(step: number) { + const [prev, next] = this.elButtons; + if (step === 0) { + prev.setAttribute('disabled', 'true'); + next.removeAttribute('disabled'); + next.focus(); + } else if (step === this.elSteps.length - 1) { + prev.removeAttribute('disabled'); + next.setAttribute('disabled', 'true'); + prev.focus(); + } else { + prev.removeAttribute('disabled'); + next.removeAttribute('disabled'); + } + } + + private setButtonLabels(step: number) { + const [prevSlot, nextSlot] = this.elButtonLabelSlots; + + const prevStep = step > 0 ? this.elSteps.item(step - 1) : null; + const nextStep = step < this.elSteps.length - 1 ? this.elSteps.item(step + 1) : null; + + prevSlot.textContent = prevStep?.textContent ?? ''; + nextSlot.textContent = nextStep?.textContent ?? ''; + } +} diff --git a/challenges/02-progress-steps/src/style.css b/challenges/02-progress-steps/src/style.css new file mode 100644 index 0000000..19f16ab --- /dev/null +++ b/challenges/02-progress-steps/src/style.css @@ -0,0 +1,179 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + font-size: 16px; + + color: #213547; + background-color: #ffffff; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +:root { + --default-color: #e0e0e0; + --active-color: #00c853; +} + +body { + margin: 0; + + display: flex; + align-items: center; + justify-content: center; + + min-width: 320px; + min-height: 100vh; +} + +#app { + width: 50vw; + min-width: 35rem; + max-width: 80rem; + + display: flex; + flex-direction: column; + justify-content: center; +} + +.progress { + --steps-count: 4; + --current-step: 0; + + display: flex; + flex-direction: column; + + border-radius: .5rem; +} + +.progress:focus { + outline-offset: 1.8rem; + outline-width: 0.25rem; + outline-color: var(--active-color); +} + +.progress__indicator { + display: flex; + justify-content: space-between; + + position: relative; + + user-select: none; + + margin: 0; + padding: 0; + list-style: none; +} + +.progress__indicator::before, +.progress__indicator::after { + content: ''; + + position: absolute; + top: 50%; + left: 0; + right: 0; + + height: 0.25rem; + + transition: width 0.3s ease; +} + +.progress__indicator::before { + background-color: var(--default-color); + + z-index: -2; +} + +.progress__indicator::after { + width: calc(100% / (var(--steps-count) - 1) * (var(--current-step))); + + background-color: var(--active-color); + + z-index: -1; +} + +.progress__indicator__circle { + width: 2rem; + height: 2rem; + + border-radius: 50%; + background-color: var(--default-color); + + display: flex; + align-items: center; + justify-content: center; + + color: #ffffff; + font-size: 1.25rem; + font-weight: 700; + + transition: background-color 0.3s ease; +} + +.progress__indicator__circle[data-active="true"] { + background-color: var(--active-color); +} + +.progress__operators { + display: flex; + justify-content: center; + align-items: center; + + margin: 5rem 0 0 0; + gap: 2rem; +} + +.progress__button { + padding: .5rem 1.8rem; + + border: none; + border-radius: 0.5rem; + + background-color: var(--active-color); + color: #ffffff; + + font-size: 1rem; + font-weight: 700; + + cursor: pointer; + + transition: background-color 0.3s ease; +} + +.progress__button:disabled { + background-color: var(--default-color); + cursor: not-allowed; +} + +.progress__button:not(:disabled):hover { + filter: brightness(1.1); +} + +.progress__button:not(:disabled):active { + transform: scale(0.95); + filter: brightness(0.99); +} + +.progress__button:focus { + outline-offset: .2rem; + outline-width: .25rem; + outline-color: var(--active-color); +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + + padding: 0; + margin: -1px; + + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} \ No newline at end of file diff --git a/challenges/02-progress-steps/src/vite-env.d.ts b/challenges/02-progress-steps/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/challenges/02-progress-steps/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/challenges/02-progress-steps/tsconfig.json b/challenges/02-progress-steps/tsconfig.json new file mode 100644 index 0000000..319652a --- /dev/null +++ b/challenges/02-progress-steps/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + // Language and environment settings + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "useDefineForClassFields": true, + + // Type-checking and declaration options + "skipLibCheck": true, + "types": ["vitest/importMeta"], + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + // Module resolution and path mapping + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "resolveJsonModule": true, + "allowImportingTsExtensions": true, + + // Emitting and isolating modules + "isolatedModules": true, + "noEmit": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/challenges/02-progress-steps/tsconfig.node.json b/challenges/02-progress-steps/tsconfig.node.json new file mode 100644 index 0000000..00411de --- /dev/null +++ b/challenges/02-progress-steps/tsconfig.node.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + // Project references + "composite": true, + + // Type-checking options + "skipLibCheck": true, + + // Module and resolution settings + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/challenges/02-progress-steps/vite.config.ts b/challenges/02-progress-steps/vite.config.ts new file mode 100644 index 0000000..d6cc4d5 --- /dev/null +++ b/challenges/02-progress-steps/vite.config.ts @@ -0,0 +1,19 @@ +/// +import { defineConfig } from 'vite'; +import tsconfigPaths from 'vite-tsconfig-paths'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [tsconfigPaths()], + server: { + port: 3000, + }, + define: { + 'import.meta.vitest': 'undefined', + }, + test: { + // enable in-source testing to bring a closer feedback loop for development + // @see https://vitest.dev/guide/in-source.html + includeSource: ['src/**/*.{ts,tsx}'], + }, +}); diff --git a/yarn.lock b/yarn.lock index 33e0d1e..9c4c541 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11,6 +11,12 @@ __metadata: languageName: unknown linkType: soft +"02-progress-steps@workspace:challenges/02-progress-steps": + version: 0.0.0-use.local + resolution: "02-progress-steps@workspace:challenges/02-progress-steps" + languageName: unknown + linkType: soft + "@aashutoshrathi/word-wrap@npm:^1.2.3": version: 1.2.6 resolution: "@aashutoshrathi/word-wrap@npm:1.2.6"