A11y: Add aria label to opening hours table, see #1181

This commit is contained in:
Pieter Vander Vennet 2023-12-15 18:14:21 +01:00
parent af4d9bb2bf
commit b7175384f9
6 changed files with 296 additions and 60 deletions

View file

@ -1,6 +1,8 @@
import { Utils } from "../../Utils"
import opening_hours from "opening_hours"
import { Store } from "../../Logic/UIEventSource"
import { Translation, TypedTranslation } from "../i18n/Translation"
import Translations from "../i18n/Translations"
export interface OpeningHour {
weekday: number // 0 is monday, 1 is tuesday, ...
@ -10,6 +12,14 @@ export interface OpeningHour {
endMinutes: number
}
export interface OpeningRange {
isOpen: boolean
isSpecial: boolean
comment: string
startDate: Date
endDate: Date
}
/**
* Various utilities manipulating opening hours
*/
@ -495,6 +505,7 @@ This list will be sorted
return [changeHours, changeHourText]
}
public static CreateOhObjectStore(
tags: Store<Record<string, string>>,
key: string = "opening_hours",
@ -533,6 +544,7 @@ This list will be sorted
[country]
)
}
public static CreateOhObject(
tags: Record<string, string> & { _lat: number; _lon: number; _country?: string },
textToParse: string,
@ -553,21 +565,156 @@ This list will be sorted
)
}
/*
Calculates when the business is opened (or on holiday) between two dates.
Returns a matrix of ranges, where [0] is a list of ranges when it is opened on monday, [1] is a list of ranges for tuesday, ...
*/
public static GetRanges(
oh: any,
from: Date,
to: Date
): {
isOpen: boolean
isSpecial: boolean
comment: string
startDate: Date
endDate: Date
}[][] {
/**
* let ranges = <any> [
* [
* {
* "isSpecial": false,
* "isOpen": true,
* "startDate": new Date("2023-12-11T09:00:00.000Z"),
* "endDate": new Date("2023-12-11T12:30:00.000Z")
* },
* {
* "isSpecial": false,
* "isOpen": true,
* "startDate": new Date("2023-12-11T13:30:00.000Z"),
* "endDate": new Date("2023-12-11T18:00:00.000Z")
* }
* ],
* [
* {
* "isSpecial": false,
* "isOpen": true,
* "startDate": new Date("2023-12-12T09:00:00.000Z"),
* "endDate": new Date("2023-12-12T12:30:00.000Z")
* },
* {
* "isSpecial": false,
* "isOpen": true,
* "startDate": new Date("2023-12-12T13:30:00.000Z"),
* "endDate": new Date("2023-12-12T18:00:00.000Z")
* }
* ],
* [
* {
* "isSpecial": false,
* "isOpen": true,
* "startDate": new Date("2023-12-13T09:00:00.000Z"),
* "endDate": new Date("2023-12-13T12:30:00.000Z")
* },
* {
* "isSpecial": false,
* "isOpen": true,
* "startDate": new Date("2023-12-13T13:30:00.000Z"),
* "endDate": new Date("2023-12-13T18:00:00.000Z")
* }
* ],
* [
* {
* "isSpecial": false,
* "isOpen": true,
* "startDate": new Date("2023-12-14T09:00:00.000Z"),
* "endDate": new Date("2023-12-14T12:30:00.000Z")
* },
* {
* "isSpecial": false,
* "isOpen": true,
* "startDate": new Date("2023-12-14T13:30:00.000Z"),
* "endDate": new Date("2023-12-14T18:00:00.000Z")
* }
* ],
* [
* {
* "isSpecial": false,
* "isOpen": true,
* "startDate": new Date("2023-12-15T09:00:00.000Z"),
* "endDate": new Date("2023-12-15T12:30:00.000Z")
* },
* {
* "isSpecial": false,
* "isOpen": true,
* "startDate": new Date("2023-12-15T13:30:00.000Z"),
* "endDate": new Date("2023-12-15T18:00:00.000Z")
* }
* ],
* [],
* []
* ]
* OH.weekdaysIdentical(ranges, 0, 1) // => true
* OH.weekdaysIdentical(ranges, 0, 4) // => true
* OH.weekdaysIdentical(ranges, 4, 5) // => false
*
*/
/**
* Constructs the opening-ranges for either this week, or for next week if there are no more openings this week
*/
public static createRangesForApplicableWeek(oh: opening_hours): {
ranges: OpeningRange[][]
startingMonday: Date
} {
const today = new Date()
today.setHours(0, 0, 0, 0)
const lastMonday = OH.getMondayBefore(today)
const nextSunday = new Date(lastMonday)
nextSunday.setDate(nextSunday.getDate() + 7)
if (!oh.getState() && !oh.getUnknown()) {
// POI is currently closed
const nextChange: Date = oh.getNextChange()
if (
// Shop isn't gonna open anymore in this timerange
nextSunday < nextChange &&
// And we are already in the weekend to show next week
(today.getDay() == 0 || today.getDay() == 6)
) {
// We move the range to next week!
lastMonday.setDate(lastMonday.getDate() + 7)
nextSunday.setDate(nextSunday.getDate() + 7)
}
}
/* We calculate the ranges when it is opened! */
return { startingMonday: lastMonday, ranges: OH.GetRanges(oh, lastMonday, nextSunday) }
}
public static weekdaysIdentical(openingRanges: OpeningRange[][], startday = 0, endday = 4) {
console.log("Checking identical:", openingRanges)
const monday = openingRanges[startday]
for (let i = startday + 1; i <= endday; i++) {
let weekday = openingRanges[i]
if (weekday.length !== monday.length) {
console.log("Mismatched length")
return false
}
for (let j = 0; j < weekday.length; j++) {
const openingRange = weekday[j]
const mondayRange = monday[j]
if (
openingRange.isOpen !== mondayRange.isOpen &&
openingRange.isSpecial !== mondayRange.isSpecial &&
openingRange.comment !== mondayRange.comment
) {
return false
}
if (
openingRange.startDate.toTimeString() !== mondayRange.startDate.toTimeString()
) {
return false
}
if (openingRange.endDate.toTimeString() !== mondayRange.endDate.toTimeString()) {
return false
}
}
}
return true
}
/**
* Calculates when the business is opened (or on holiday) between two dates.
* Returns a matrix of ranges, where [0] is a list of ranges when it is opened on monday, [1] is a list of ranges for tuesday, ...
*/
public static GetRanges(oh: opening_hours, from: Date, to: Date): OpeningRange[][] {
const values = [[], [], [], [], [], [], []]
const start = new Date(from)
@ -607,6 +754,13 @@ Returns a matrix of ranges, where [0] is a list of ranges when it is opened on m
return values
}
public static getMondayBefore(d) {
d = new Date(d)
const day = d.getDay()
const diff = d.getDate() - day + (day == 0 ? -6 : 1) // adjust when day is sunday
return new Date(d.setDate(diff))
}
/**
* OH.parseHHMM("12:30") // => {hours: 12, minutes: 30}
*/
@ -741,11 +895,104 @@ Returns a matrix of ranges, where [0] is a list of ranges when it is opened on m
}
return ohs
}
}
public static getMondayBefore(d) {
d = new Date(d)
const day = d.getDay()
const diff = d.getDate() - day + (day == 0 ? -6 : 1) // adjust when day is sunday
return new Date(d.setDate(diff))
export class ToTextualDescription {
public static createTextualDescriptionFor(
oh: opening_hours,
ranges: OpeningRange[][]
): Translation {
const t = Translations.t.general.opening_hours
if (!ranges?.some((r) => r.length > 0)) {
// <!-- No changes to the opening hours in the next week; probably open 24/7, permanently closed, opening far in the future or unkown -->
if (oh.getNextChange() === undefined) {
// <!-- Permenantly in the same state -->
if (oh.getComment() !== undefined) {
return new Translation({ "*": oh.getComment() })
}
if (oh.getUnknown()) {
return t.unknown
}
if (oh.getState()) {
return t.open_24_7
} else {
return t.closed_permanently
}
}
}
// Opened at a more-or-less normal, weekly rhythm
if (OH.weekdaysIdentical(ranges, 0, 6)) {
return t.all_days_from.Subs({ ranges: ToTextualDescription.createRangesFor(ranges[0]) })
}
if (OH.weekdaysIdentical(ranges, 0, 4) && OH.weekdaysIdentical(ranges, 5, 6)) {
let result = []
if (ranges[0].length > 0) {
result.push(
t.on_weekdays.Subs({ ranges: ToTextualDescription.createRangesFor(ranges[0]) })
)
}
if (ranges[6].length > 0) {
result.push(
t.on_weekdays.Subs({ ranges: ToTextualDescription.createRangesFor(ranges[5]) })
)
}
return ToTextualDescription.chain(result)
}
const result: Translation[] = []
const weekdays = [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
]
for (let i = 0; i < weekdays.length; i++) {
const day = weekdays[i]
console.log(day, "-->", ranges[i])
if (ranges[i]?.length > 0) {
result.push(
t[day].Subs({ ranges: ToTextualDescription.createRangesFor(ranges[i]) })
)
}
}
return ToTextualDescription.chain(result)
}
private static chain(trs: Translation[]): Translation {
let chainer = new TypedTranslation<{ a; b }>({ "*": "{a}. {b}" })
let tr = trs[0]
for (let i = 1; i < trs.length; i++) {
tr = chainer.Subs({ a: tr, b: trs[i] })
}
return tr
}
private static timeString(date: Date) {
return OH.hhmm(date.getHours(), date.getMinutes())
}
private static createRangeFor(range: OpeningRange): Translation {
console.log(">>>", range)
return Translations.t.general.opening_hours.ranges.Subs({
starttime: ToTextualDescription.timeString(range.startDate),
endtime: ToTextualDescription.timeString(range.endDate),
})
}
private static createRangesFor(ranges: OpeningRange[]): Translation {
let tr = ToTextualDescription.createRangeFor(ranges[0])
for (let i = 1; i < ranges.length; i++) {
tr = Translations.t.general.opening_hours.rangescombined.Subs({
range0: tr,
range1: ToTextualDescription.createRangeFor(ranges[i]),
})
}
return tr
}
}

View file

@ -1,7 +1,7 @@
import { UIEventSource } from "../../Logic/UIEventSource"
import Combine from "../Base/Combine"
import { FixedUiElement } from "../Base/FixedUiElement"
import { OH } from "./OpeningHours"
import { OH, OpeningRange, ToTextualDescription } from "./OpeningHours"
import Translations from "../i18n/Translations"
import BaseUIElement from "../BaseUIElement"
import Toggle from "../Input/Toggle"
@ -10,6 +10,7 @@ import Table from "../Base/Table"
import { Translation } from "../i18n/Translation"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Loading from "../Base/Loading"
import opening_hours from "opening_hours"
export default class OpeningHoursVisualization extends Toggle {
private static readonly weekdays: Translation[] = [
@ -41,7 +42,21 @@ export default class OpeningHoursVisualization extends Toggle {
if (opening_hours_obj === "error") {
return Translations.t.general.opening_hours.error_loading
}
return OpeningHoursVisualization.CreateFullVisualisation(opening_hours_obj)
const applicableWeek = OH.createRangesForApplicableWeek(opening_hours_obj)
const textual = ToTextualDescription.createTextualDescriptionFor(
opening_hours_obj,
applicableWeek.ranges
)
const vis = OpeningHoursVisualization.CreateFullVisualisation(
opening_hours_obj,
applicableWeek.ranges,
applicableWeek.startingMonday
)
textual.current.addCallbackAndRunD((descr) => {
vis.ConstructElement().ariaLabel = descr
})
return vis
})
)
@ -53,33 +68,11 @@ export default class OpeningHoursVisualization extends Toggle {
this.SetClass("no-weblate")
}
private static CreateFullVisualisation(oh: any): BaseUIElement {
/** First, we determine which range of dates we want to visualize: this week or next week?**/
const today = new Date()
today.setHours(0, 0, 0, 0)
const lastMonday = OH.getMondayBefore(today)
const nextSunday = new Date(lastMonday)
nextSunday.setDate(nextSunday.getDate() + 7)
if (!oh.getState() && !oh.getUnknown()) {
// POI is currently closed
const nextChange: Date = oh.getNextChange()
if (
// Shop isn't gonna open anymore in this timerange
nextSunday < nextChange &&
// And we are already in the weekend to show next week
(today.getDay() == 0 || today.getDay() == 6)
) {
// We move the range to next week!
lastMonday.setDate(lastMonday.getDate() + 7)
nextSunday.setDate(nextSunday.getDate() + 7)
}
}
/* We calculate the ranges when it is opened! */
const ranges = OH.GetRanges(oh, lastMonday, nextSunday)
private static CreateFullVisualisation(
oh: opening_hours,
ranges: OpeningRange[][],
lastMonday: Date
): BaseUIElement {
/* First, a small sanity check. The business might be permanently closed, 24/7 opened or something other special
* So, we have to handle the case that ranges is completely empty*/
if (ranges.filter((range) => range.length > 0).length === 0) {

View file

@ -17,6 +17,8 @@
export let layer: LayerConfig
export let config: TagRenderingConfig
export let extraClasses: string | undefined = undefined
export let id : string = undefined
if (config === undefined) {
throw "Config is undefined in tagRenderingAnswer"
@ -26,7 +28,7 @@
</script>
{#if config !== undefined && (config?.condition === undefined || config.condition.matchesProperties($tags))}
<div class={twMerge("link-underline inline-block w-full", config?.classes, extraClasses)}>
<div {id} class={twMerge("link-underline inline-block w-full", config?.classes, extraClasses)}>
{#if $trs.length === 1}
<TagRenderingMapping mapping={$trs[0]} {tags} {state} {selectedElement} {layer} />
{/if}

View file

@ -102,9 +102,7 @@
</TagRenderingQuestion>
{:else}
<div class="low-interaction flex items-center justify-between overflow-hidden rounded px-2">
<div id={answerId}>
<TagRenderingAnswer {config} {tags} {selectedElement} {state} {layer} />
</div>
<TagRenderingAnswer id={answerId} {config} {tags} {selectedElement} {state} {layer} />
<button
on:click={() => {
editMode = true

View file

@ -24,6 +24,7 @@
| "small-height"
| "medium-height"
| "large-height"
| string
}
</script>

View file

@ -819,13 +819,8 @@ export default class SpecialVisualizations {
example:
"A normal opening hours table can be invoked with `{opening_hours_table()}`. A table for e.g. conditional access with opening hours can be `{opening_hours_table(access:conditional, no @ &LPARENS, &RPARENS)}`",
constr: (state, tagSource: UIEventSource<any>, args) => {
return new OpeningHoursVisualization(
tagSource,
state,
args[0],
args[1],
args[2]
)
const [key, prefix, postfix] = args
return new OpeningHoursVisualization(tagSource, state, key, prefix, postfix)
},
},
{