Skip to content

A tour that branches

Two buttons choose the way on, each way is a short story of its own, and both hand the tour to a summary once the order is sent.

The story
// What a branch assumes: nothing sent yet, and nothing read yet. Both are the
// page's own state rather than a variable this file keeps, which is also where
// the chosen path is kept.
const fresh = (): void => {
;(at('[data-checked]') as HTMLInputElement).checked = false
const status = at('[data-status]')
status.textContent = 'Nothing sent yet.'
status.dataset['sent'] = 'no'
}
const intro = {
id: 'intro',
// Asked when `choose` advances, and asked again on every run. The page holds
// which button was pressed, so nothing is written on this object and a second
// run of the tour cannot inherit the first run's branch.
next: () => (at('[data-panel]').dataset['path'] === 'quick' ? quick : careful),
steps: [
{
id: 'total',
target: '[data-total]',
message: 'Where every path starts. Press Next.',
},
{
id: 'choose',
target: { elements: ['[data-careful]', '[data-quick]'], interactive: true },
// A control would be a way past the choice, so this step has none and the
// buttons report instead. Which story that opens is `next`'s answer.
awaits: 'path-chosen',
message:
'Two ways on. Press either. The page says which was pressed and ' +
'this story says what follows it, so watch the footer: the switch ' +
'cuts rather than morphs, because two unrelated stories ' +
'interpolating into each other would be a strange thing to watch.',
},
],
} satisfies LekoStory
const summary = {
// Where the paths meet — DESIGN.md, **What two paths share is a story,
// not a step they both point at**. It says what it needs in its own
// `onEnter`.
//
// No `next`. The tour ends here, and running out of steps with nothing after
// it is how a tour ends.
id: 'summary',
steps: [
{
id: 'summary',
target: '[data-status]',
message:
'The rejoin, and a story of its own. Both branches name it in ' +
'‘next’, so neither of them had to know how many steps came ' +
'before it.',
},
],
} satisfies LekoStory
const careful = {
id: 'careful',
next: summary,
onEnter: fresh,
steps: [
{
id: 'lines',
// The box is the target, because ticking it is the work. The rows
// it is a claim about get a cutout of their own rather than joining
// the union, so the space between the two stays dimmed and stays
// blocked.
target: [{ elements: '[data-check]', interactive: true }, '[data-lines]'],
message:
'careful 1/2 in the footer. A branch counts from one, because ' +
'the count belongs to the story that is running.',
awaits: 'lines-checked',
},
{
id: 'send',
target: { elements: '[data-send]', interactive: true },
message: 'Send it. That ends this branch, and the branch says what follows it.',
awaits: 'order-sent',
},
],
} satisfies LekoStory
const quick = {
id: 'quick',
next: summary,
onEnter: fresh,
steps: [
{
id: 'send',
target: { elements: '[data-send]', interactive: true },
message: 'quick 1/1. Same button, same signal, same rejoin, one step to get there.',
awaits: 'order-sent',
},
],
} satisfies LekoStory
What the page reports
// The page records which way it went and says that the choice was made.
// Which story that opens is `intro`'s to answer, so neither branch is named
// here and a third one would be added to `next` rather than to this file's
// event listeners.
for (const path of ['careful', 'quick'] as const) {
panel.querySelector(`[data-${path}]`)!.addEventListener('click', () => {
panel.dataset['path'] = path
leko.reached('path-chosen')
})
}
checkbox.addEventListener('change', () => {
if (checkbox.checked) leko.reached('lines-checked')
})
panel.querySelector('[data-send]')!.addEventListener('click', () => {
status.textContent = 'Order sent.'
status.dataset['sent'] = 'yes'
// One call, at the point the order goes. Both branches end on it, and
// neither of them is named here.
leko.reached('order-sent')
})