Skip to content

Commit f4f5eea

Browse files
committed
Add exercise files
1 parent 07a85f2 commit f4f5eea

File tree

16 files changed

+1002
-13
lines changed

16 files changed

+1002
-13
lines changed

App.js

+8-8
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ import SubmitListingPage from "components/page-submit-listing"
1515
import HikesPage from "components/page-adventures-hikes"
1616
import Exercise1ListingsPage from "./exercise1-headings-landmarks/page-listings"
1717
import Exercise1ListingPage from "./exercise1-headings-landmarks/page-listing-detail"
18-
import ARIAExercise from "./exercise2-what-is-aria/page-listing-detail"
19-
import A11yNamingExercise from "./exercise3-accessible-names/page-listing-detail"
20-
import A11yNamingExerciseListings from "./exercise3-accessible-names/page-listings"
21-
import ProgrammaticA11yExercise from "./exercise4-programmatic-a11y-info/page-listing-detail"
18+
import Exercise2ARIAListingPage from "./exercise2-what-is-aria/page-listing-detail"
19+
import Exercise3NamesListingPage from "./exercise3-accessible-names/page-listing-detail"
20+
import Exercise3NamesListingsPage from "./exercise3-accessible-names/page-listings"
21+
import Exercise4A11yInfoListingPage from "./exercise4-programmatic-a11y-info/page-listing-detail"
2222

2323
import imgFooterLogo from "images/icons/footer-logo.svg"
2424

@@ -39,10 +39,10 @@ export function App() {
3939
<TripIdeasPage path="/trip-ideas" />
4040
<Exercise1ListingsPage path="/exercise1/listings" />
4141
<Exercise1ListingPage path="/exercise1/:id" />
42-
<ARIAExercise path="/exercise2/:id" />
43-
<A11yNamingExercise path="/exercise3/:id" />
44-
<A11yNamingExerciseListings path="/exercise3/listings" />
45-
<ProgrammaticA11yExercise path="/exercise4/:id" />
42+
<Exercise2ARIAListingPage path="/exercise2/:id" />
43+
<Exercise3NamesListingPage path="/exercise3/:id" />
44+
<Exercise3NamesListingsPage path="/exercise3/listings" />
45+
<Exercise4A11yInfoListingPage path="/exercise4/:id" />
4646
</Router>
4747
</div>
4848
<div id="footer">

components/icon.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, {useState} from "react"
2-
import './styles/icons.scss'
2+
import 'components/styles/icons.scss'
33

44
const Icon = ({name, showText = false}) => {
55
const StaticOrInteractive = showText ? `span` : `button`

exercise1-headings-landmarks/README.md

+6-4
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,17 @@ of support in browsers and assistive technologies.
1010

1111
## Exercise: Build the listing detail page template with semantics and heading structure
1212

13-
Using the `page-listing-detail.js` file in `exercise1-headings-landmarks`
14-
as a reference, build the Page Listing Detail template in `components` for a
15-
campground using semantic landmarks and headings.
13+
Using the files in `exercise1-headings-landmarks`
14+
as a reference, adjust the markup in the Listings Page and Listing Detail template
15+
in `components` using semantic landmarks and headings.
1616

17-
The application already has dynamic routing set up for each of the campgrounds listed in
17+
The application has dynamic routing set up for each of the campgrounds listed in
1818
`data/listings.json`. You can get to the campground listings from "Plan Your Trip >
1919
Find a Camping Spot" in the main navigation, and the individual listing details from there.
2020

2121
Visit the before and after pages by URL:
2222

23+
- http://localhost:1234/listings
24+
- http://localhost:1234/exercise1/listings
2325
- http://localhost:1234/listing/listing-cranberry-lake
2426
- http://localhost:1234/exercise1/listing-cranberry-lake

exercise2-what-is-aria/README.md

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# What is ARIA and how do you use it?
2+
3+
The first rule of ARIA (Accessible Rich Internet Applications) is "Don't use ARIA". When the time comes and you do need it, you'll want to know you're doing it right. This part of the workshop discusses the various roles, states, and properties of ARIA. In addition, you'll learn about accessibility APIs and how assistive technologies interact with web applications.
4+
5+
ARIA comes from a specification that you can visit and bookmark in your browser: https://www.w3.org/TR/wai-aria-1.1/
6+
7+
The specification lists a standard set of attributes that you can plumb into your webpages for accessibility information. "Standard" means you can't make up your own attributes, and there are requirements for using them on specific elements or in combinations.
8+
9+
## Exercise: Inspect date picker with DIVs and buttons, add states & properties
10+
11+
Using the `date-picker.js` file in `exercise2-what-is-aria` as
12+
a reference, play around with the semantics of the date picker. For this
13+
exercise, pay close attention to roles, states, and properties.
14+
15+
As you navigate the original date picker with a screen reader, pay close attention to how the month and date buttons are announced. Are you able to
16+
reach and operate them? Is it clear which dates are already
17+
booked or selected? How might you communicate that information with ARIA?
18+
19+
Make changes to the component in `components/date-picker/date-picker.js`
20+
and test your changes in the browser with VoiceOver and/or NVDA.
21+
22+
Visit the before and after pages by URL:
23+
24+
- http://localhost:1234/listing/listing-cranberry-lake
25+
- http://localhost:1234/exercise2/listing-cranberry-lake

exercise2-what-is-aria/date-picker.js

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import React, { useEffect, useRef, useState } from "react"
2+
import dayjs from 'dayjs'
3+
import weekday from 'dayjs/plugin/weekday'
4+
import weekOfYear from 'dayjs/plugin/weekOfYear'
5+
6+
import { createActiveMonthDays, createPrevMonthDays, createNextMonthDays } from 'components/date-picker/utils'
7+
import "components/date-picker/date-picker.scss"
8+
9+
const DatePicker = ({monthsInAdvance = 2, currDate}) => {
10+
dayjs.extend(weekday)
11+
dayjs.extend(weekOfYear)
12+
13+
// set date to 3 months from now
14+
let date = new Date()
15+
date.setMonth(date.getMonth() + monthsInAdvance)
16+
17+
// keep the active, visible date in State
18+
let [activeDate, setActiveDate] = useState(dayjs(date))
19+
const startYear = dayjs(activeDate).format("YYYY")
20+
const startMonth = dayjs(activeDate).format("M")
21+
22+
// this collection of dates would come from a database, etc.
23+
let initUnavailableDates = ['2022-04-10', '2022-04-11', '2022-04-12', '2022-04-14', '2022-04-15', '2022-04-17', '2022-04-18', '2022-04-19', '2022-04-24', '2022-04-25', '2022-04-27']
24+
let activeMonthDays = createActiveMonthDays(startYear, startMonth, initUnavailableDates)
25+
let prevMonthDays = createPrevMonthDays(startYear, startMonth, activeMonthDays, initUnavailableDates)
26+
let nextMonthDays = createNextMonthDays(startYear, startMonth, activeMonthDays, initUnavailableDates)
27+
28+
let days = [...prevMonthDays, ...activeMonthDays, ...nextMonthDays]
29+
let [unavailableDates, setUnavailableDates] = useState(initUnavailableDates)
30+
let [selectedDates, setSelectedDates] = useState([])
31+
32+
const setPrevMonth = () => {
33+
// only go backward as far as current month
34+
if (isPrevMonthAvailable()) {
35+
setActiveDate(dayjs(activeDate).subtract(1, "month"))
36+
}
37+
}
38+
const setNextMonth = () => {
39+
setActiveDate(dayjs(activeDate).add(1, "month"))
40+
}
41+
const isPrevMonthAvailable = () => {
42+
return dayjs(activeDate).subtract(1, 'month').get('month') >= dayjs().get('month')
43+
}
44+
const isDayUnavailable = (day) => {
45+
return unavailableDates.includes(day.date)
46+
}
47+
const bookDay = (day) => {
48+
// this function would run on "Reserve"
49+
setUnavailableDates(
50+
unavailableDates => [day.date, ...unavailableDates, `${unavailableDates.length}`]
51+
)
52+
}
53+
const isDaySelected = (day) => {
54+
return selectedDates.includes(day.date)
55+
}
56+
const selectDay = (day) => {
57+
// to-do: consider perf of this for large quanitites of dates
58+
if (!isDayUnavailable(day)) {
59+
// add to selected Dates if not already selected
60+
if (!isDaySelected(day)) {
61+
setSelectedDates(
62+
selectedDates => [day.date, ...selectedDates]
63+
)
64+
} else {
65+
setSelectedDates(
66+
selectedDates.filter(date => date !== day.date)
67+
)
68+
}
69+
}
70+
}
71+
return (
72+
<div className="date-picker">
73+
<header>
74+
<button
75+
className="btn-month btn-prev"
76+
disabled={isPrevMonthAvailable() ? '' : 'disabled'}
77+
onClick={setPrevMonth}
78+
>
79+
<span></span>
80+
{ dayjs(activeDate).subtract(1, "month").format("MMM") }
81+
</button>
82+
<h4>{ dayjs(activeDate).format("MMMM YYYY") }</h4>
83+
<button
84+
className="btn-month btn-next"
85+
onClick={setNextMonth}
86+
>
87+
{ dayjs(activeDate).add(1, 'month').format("MMM") }
88+
<span></span>
89+
</button>
90+
</header>
91+
<div className="days-of-week">
92+
<span title="Sunday">S</span>
93+
<span title="Monday">M</span>
94+
<span title="Tuesday">T</span>
95+
<span title="Wednesday">W</span>
96+
<span title="Thursday">T</span>
97+
<span title="Friday">F</span>
98+
<span title="Saturday">S</span>
99+
</div>
100+
<div className="date-grid">
101+
{days.map((day, index) => {
102+
return <button
103+
aria-label={
104+
`${dayjs(day.date).format('MMMM D')}${day.isBooked ? ' already booked' : '' }${isDaySelected(day) ? ' selected' : ''}`
105+
}
106+
aria-disabled={day.isBooked ? 'true' : 'false'}
107+
aria-selected={
108+
isDaySelected(day) ? 'true' : 'false'
109+
}
110+
className={[
111+
'grid-btn',
112+
day.isBooked ? 'booked' : '',
113+
day.isCurrentMonth ? 'currentMonth' : '',
114+
isDaySelected(day) ? 'selected' : ''
115+
].join(' ').trim()}
116+
key={index}
117+
onClick={() => selectDay(day)}
118+
>
119+
<time date-time={day.date}>{day.dayOfMonth}</time>
120+
<span className="icon" aria-hidden="true"></span>
121+
</button>
122+
})}
123+
</div>
124+
<ul className="date-key" role="list">
125+
<li className="date-key-item-wrap">
126+
<span className="date-key-item booked">
127+
<span className="icon" aria-hidden="true"></span>
128+
</span>
129+
<span className="date-key-text">Booked</span>
130+
</li>
131+
<li className="date-key-item-wrap">
132+
<span className="date-key-item available">
133+
<span className="icon" aria-hidden="true"></span>
134+
</span>
135+
<span className="date-key-text">Available</span>
136+
</li>
137+
<li className="date-key-item-wrap">
138+
<span className="date-key-item selected">
139+
<span className="icon" aria-hidden="true"></span>
140+
</span>
141+
<span className="date-key-text">Selected</span>
142+
</li>
143+
</ul>
144+
<button className="reserve-btn">Reserve</button>
145+
</div>
146+
)
147+
}
148+
149+
export default DatePicker
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React from "react"
2+
import BodyClassName from "react-body-classname"
3+
import sanitizeHtml from "sanitize-html"
4+
import {Helmet} from "react-helmet"
5+
import LoadedImageUrl from "components/utils/loaded-image-url"
6+
7+
import "components/styles/page-listings.scss"
8+
9+
import HeaderPortal from "components/header-portal"
10+
import Icon from "components/icon"
11+
import ListingsData from "data/listings.json"
12+
import DatePicker from "./date-picker"
13+
14+
import * as imageURLs from "../images/listings/*.{png,jpg}"
15+
16+
const Exercise2ARIAListingPage = props => {
17+
const data = ListingsData.listings[props.id]
18+
const headerImageUrl = LoadedImageUrl(imageURLs, data.detailHeaderImageSrc)
19+
return (
20+
<BodyClassName className="header-overlap page-listing-detail">
21+
<>
22+
<HeaderPortal>
23+
<h1 className="visually-hidden">Camp Spots</h1>
24+
</HeaderPortal>
25+
<article>
26+
<header
27+
className="page-header"
28+
style={{backgroundImage: `url(${headerImageUrl}`}}
29+
>
30+
<div className="page-header-content wide-layout">
31+
<h2 className="listing-name">{data.listingName}</h2>
32+
<p className="location">{data.location}</p>
33+
</div>
34+
</header>
35+
<section className="wide-layout two-parts-70-30" aria-label="Site description and booking calendar">
36+
<div>
37+
<h3 className="h4-style">Description</h3>
38+
<div className="description-text" dangerouslySetInnerHTML={{__html: sanitizeHtml(data.description)}} />
39+
40+
<h3 className="h4-style">Amenities</h3>
41+
<ul className="amenity-icons grid">
42+
{data.amenities.map((amenity, index) => {
43+
return <li key={index}>
44+
<Icon name={amenity} showText={true} />
45+
</li>
46+
})}
47+
</ul>
48+
</div>
49+
<div>
50+
<h3 className="h4-style">Calendar</h3>
51+
<DatePicker />
52+
</div>
53+
</section>
54+
<section className="wide-layout" aria-label="Image gallery">
55+
<div className="detail-images">
56+
{data.detailImages.map((image, index) => {
57+
let detailImageUrl = LoadedImageUrl(imageURLs, image.imageSrc)
58+
return <img
59+
key={index}
60+
src={detailImageUrl}
61+
alt={image.imageAlt}
62+
/>
63+
})}
64+
</div>
65+
</section>
66+
</article>
67+
</>
68+
</BodyClassName>
69+
)
70+
}
71+
72+
export default Exercise2ARIAListingPage

exercise3-accessible-names/README.md

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Accessible Names
2+
3+
Naming things is hard. Here you'll learn the about crafting accessible names and descriptions for assistive technology, as well as the best approach to working with labels and placeholders for forms.
4+
5+
An accessible name exposes a unique name for an interactive element or landmark to describe its purpose. Things that can contribute to an accessible name include `textContent` (including children), `aria-label`, `aria-labelledby`, a `label` paired with a form control, and even `title` or
6+
`placeholder`. How text is exposed as an accessible name is determined by the Accessible Name
7+
and Description Calculation specification, found in https://www.w3.org/TR/accname-1.2/.
8+
9+
A note about `placeholder`: these aren't good to rely on for labeling as they aren't
10+
visually persistent. Even though placeholder is technically exposed as a last resort and passes
11+
automated testing tool checks, it will clear out when typing in a form field and users will forget
12+
what they're trying to fill in. A visual label is better.
13+
14+
Similarly, `title` attributes are technically exposed to assistive technology but they only show
15+
on mouse hover. That shouldn't be all that sighted users have to go on when trying to understand the
16+
purpose of your graphical elements. Use text labels with your icons, or at least make them configurable.
17+
18+
## Exercise: Write an accessible name for an icon button in two ways
19+
20+
Using the `<Icon>` component, play with different approaches for exposing an accessible name.
21+
The goal is to add a name to the component that reflects the graphic icon inside.
22+
23+
You can compare the "before" `icon.js` component with the completed one in `exercise3-accessible-names`.
24+
25+
There are a few approaches for exposing a name for an icon:
26+
27+
- If wrapped in a button, put an `aria-label` on the button itself
28+
- Put an `aria-label` on a graphical icon child element like an `img` or a `span[role=img]`
29+
- Use a `.visually-hidden` span or other child element with `textContent` inside
30+
(source in [`styles.scss`](https://github.com/marcysutton/testing-accessibility-demos/blob/main/workshop3-semantics-aria/styles.scss#L4))
31+
32+
There are even more options when the icon is SVG. If the element provides rich content, it can
33+
contain text that is exposed as a name. If the SVG is essentially an image, you can use the same
34+
graphical image approach as above. Or you can decide to mark the SVG with `role=presentation` and
35+
let the wrapping button element provide a name somehow.
36+
37+
You can visit the before and after pages containing icon components by URL:
38+
39+
- http://localhost:1234/listings
40+
- http://localhost:1234/listing/listing-cranberry-lake
41+
- http://localhost:1234/exercise3/listing-cranberry-lake
42+
- http://localhost:1234/exercise3/listings

0 commit comments

Comments
 (0)