Live site: https://jeanwll.github.io/static-job-listings-master/
This is a solution to the Job listings with filtering challenge on Frontend Mentor. Frontend Mentor challenges help you improve your coding skills by building realistic projects.
Users should be able to:
- View the optimal layout for the site depending on their device's screen size
- See hover states for all interactive elements on the page
- Filter job listings based on the keywords
- [Added] Reload jobs if any error is raised during data fetching Error demo
- [Added] Save filters in localStorage
- Semantic HTML5 markup
- CSS custom properties
- Flexbox
- Object Oriented JavaScript
- ArrowJS - Reactive UI with native JavaScript
I picked this project because it was a simple enough data structure to try to setup a JS data-binding system.
After messing up on my own with Object.defineProperty
and Proxy
, I realized it was not a simple task.
I searched for a zero dependencies, modern JavaScript and lightweight data-binding tool, I came across Reef and ArrowJS.
I chose ArrowJS because I slightly preferred the way it handled events.
ArrowJS is still in an early stage of development, there was not much project to be inspired from or threads of common questions to learn to use it.
I spent a good amount of time figuring out how to use it and how I wanted to use it.
Besides the JavaScript part, I also spent a lot of time testing ARIA and accessibility practices on different screen readers, mainly NVDA and Windows Narrator.
I fully rewrote the HTML markup after those experiments.
- JS Class as UI compononents (Filters, Jobs)
const filters = new Filters()
const jobs = new Jobs(filters)
html`
${filters.render()}
${jobs.render()}
`(document.body)
- Divided component parts rendering into different
render*()
class methods
class Jobs {
...
render() {}
renderJobs() {}
renderDescription(job) {}
renderKeywords(job) {}
}
class Filters {
...
toggle(filter) {
const { filters } = this.data
const index = filters.indexOf(filter)
if (index < 0) filters.push(filter)
else filters.splice(index, 1)
}
...
renderFilters() {
return this.data.filters.map(filter => {
return html`
<li class="filter">
<span>${filter}</span>
<button class="filter__btn" aria-controls="jobs"
@click="${() => this.toggle(filter)}">
<span class="visually-hidden">Remove filter</span>
</button>
</li>
`
})
}
}
visually-hidden
CSS class is more reliable thanaria-label
.
aria-label
should be well supported for buttons, but I used visually-hidden
over aria-label
for consistency.
When to use aria-label or screen reader only text
-
aria-live
on the jobs list section to suggest dynamic content. -
aria-controls="jobs"
on filtering and keywords toggle buttons to specify elements association. -
aria-busy
on the job list to convey pending data fetching. -
aria-pressed
on keywords toggle buttons
Using previously mentionned aria attributes and properties is a great way to write meaningful CSS rulesets.
#jobs[aria-busy] .jobs__error,
.jobs:not(:empty) + .jobs__error {
display: none;
}
.keyword__btn[aria-pressed=true] {}
.keyword__btn[aria-pressed=false] {}
Truncation is not a content strategy
My first approach for the layout to adapt to user-generated content (Company name, Job title, keywords list) was to use text-overflow: ellipsis
paired with title
attribute.
Furthering my research on accessibility, I realized title
wouldn't be helpful on touch-based devices.
This is when I came across the above article and decided to embrace asymmetric design.
Here is an example using unusual flexbox combinations:
.job__top {
display: flex;
flex-direction: row-reverse;
flex-wrap: wrap;
justify-content: flex-end;
align-items: center;
row-gap: 10px;
}
Notice 'new' and 'preview' tags wrapping on top instead of below:
The design currently doesn't convey a very clear call to action aka where to click if you want more info about the job.
I am unsure about the html semantic used to describe the job.
Something that came to my mind was the definition list <dl>
.
However after further researches I figured "Position, Company, ..." are not really terms.
Using simple unordered list <ul>
would make the layout very difficult to achieve, especially while implementing asymmetric design, since you can't have generic <div>
between <ul>
and <li>
.
I decided to simply use a succession of paragraphs <p>
.