package gallery import ( "html/template" "strings" "amadan.net/rastrillo/fiesta" ) // The rail's model: one section per page kind with the entries the // page anchors, read off the same data the page renders from, so an // entry pointing at nothing and a section with no way to reach it are // both impossible. The rail is the same on every page apart from the // current marks: one list of the whole vocabulary rather than a table // of contents per page, which TestTheRailIsTheSameOnEveryPage holds // literally. The phone index is read off the rail, so the rows and // the tree cannot list different sections. // navItem is one line in the rail. Code marks an entry whose label is // an identifier (an example's name, an icon's slug), drawn in the mono // face. Blank marks an entry that leaves the page and opens in a new // tab. Aria is the accessible name where it has to differ from the // visible label; Terms the synonyms the filter matches beside it. // Current marks the entry that is the page being rendered: an // example's own line on its page. type navItem struct { Label, Href string Code, Blank, Current bool Aria, Terms string } // searchTerms are the words people call an example by that its name // does not contain, keyed by example name and matched by the filter // beside the name. Rastrillo keys them by anchor id; Fiesta's rail // entries are example pages, so the key is the page's stem. // TestEverySearchTermNamesAnAnchor holds every key to a real example // and every rail link to its terms. var searchTerms = map[string]string{ "field-check": "checkbox toggle switch", "modal": "dialog popup", "choice-field": "radio", "list-grid": "table", "box": "card panel", "person": "avatar user", "button": "button submit cta", "snippet": "code copy clipboard", "side-nav": "sidebar navigation sections", } // shellNames are the shells the Demos group opens, in the order the // layout examples are read: the five whose frame document is an app // laid out whole. The -page pair members are the content pages of the // sidebar and console shells and are reached from inside those. var shellNames = []string{"column", "topbar", "sidebar", "console", "stage"} // navSection is one group in the rail: a page of the directory, or // the Demos. Href is the section's own page and Current says whether // that page is the one being rendered. type navSection struct { Title, Href, Kind string Current bool Items []navItem } // indexRow is one row of the phone index: a section's page, by title, // carrying the id its pages' back controls name. type indexRow struct{ ID, Title, Href string } // localeLink is one entry of the language menu: the locale's code, its // name in its own language, its direction, this page in that locale, // and whether it is the page's own. type localeLink struct { Code, Name, Dir, Href string Current bool } // controls is one copy of the switchers: the theme tabs, the scheme // menu (Fiesta's, lifted from the scheme-menu example, so fiesta.js // owns the scheme) and the language menu. Every page writes them for // the bar and, on a phone, for the display settings menu, from this // one value, so the two cannot differ. type controls struct { Themes []link Scheme template.HTML Locales []localeLink LocaleName string } // tokenNav is one entry per token group on the Tokens page, in page // order: the theme's palette groups, then the structure every theme // shares. func (s *site) tokenNav(p page) []navItem { file := s.href(p.Theme, p.Locale, s.fileOf("tokens")) var items []navItem for _, g := range s.tokenGroups(p.Theme) { items = append(items, navItem{Label: s.say(p.Locale, "gallery.tokens.group."+g.Key), Href: file + "#" + anchorID("tokens", g.Key)}) } return items } // iconNav is one entry per icon, by slug, in the order the page draws // them. func (s *site) iconNav(p page) []navItem { file := s.href(p.Theme, p.Locale, s.fileOf("icons")) var items []navItem for _, ic := range s.icons { items = append(items, navItem{Label: ic.Slug, Href: file + "#" + anchorID("icon", ic.Slug), Code: true}) } return items } // familyNav is one family's entries: its examples, by name, each the // link to its own page. It closes over the key because the rail is the // same on every page: every family's section lists that family's // examples wherever the reader is standing. func (s *site) familyNav(key string) func(p page) []navItem { return func(p page) []navItem { var items []navItem for _, f := range s.families { if f.Key != key { continue } for _, e := range f.Examples { items = append(items, navItem{Label: e.Name, Href: s.href(p.Theme, p.Locale, e.Name+".html"), Code: true}) } } return items } } // controls builds the switchers for one page: this page in every // theme, the scheme menu, and this page in every locale, each language // named in itself. The links carry no fragment; the script adds the // reader's place at the click. func (s *site) controls(p page) controls { c := controls{Scheme: s.schemeIn(p.Locale), LocaleName: s.say(p.Locale, "gallery.locale.name")} for _, theme := range fiesta.ThemeNames() { c.Themes = append(c.Themes, link{Href: s.href(theme, p.Locale, p.File), Label: theme, Current: theme == p.Theme}) } for _, code := range Locales { c.Locales = append(c.Locales, localeLink{ Code: code, Name: endonym(code), Dir: Dir(code), Href: s.href(p.Theme, code, p.File), Current: code == p.Locale, }) } return c } // galleryNav is the rail for one page: one section per page kind, in // table order, each with its overview item first and then the entries // its page anchors, with the search terms on the entries that have // them; then the Demos. The section whose kind is the page's is // Current (on an example page that is its family's), and on an // example page the example's own entry is Current too. func (s *site) galleryNav(p page) []navSection { self := s.href(p.Theme, p.Locale, p.File) out := make([]navSection, 0, len(s.pageKinds())+1) for _, pk := range s.pageKinds() { section := navSection{ Title: s.titleOf(p.Locale, pk), Href: s.href(p.Theme, p.Locale, pk.File), Kind: pk.Kind, Current: pk.Kind == p.Kind, } if pk.Nav != nil { // The section overview link, first, before the entries. A // section that lists anything draws its title as a // , which discloses rather than navigates, so // without this the section would have no way to its own // page. A section with nothing to list is already a plain // link to its page and needs no second one under it. section.Items = append([]navItem{s.sectionOverview(p, section)}, pk.Nav(p)...) for i := 1; i < len(section.Items); i++ { item := §ion.Items[i] item.Terms = searchTerms[stemOf(item.Href)] item.Current = item.Href == self } } out = append(out, section) } return append(out, s.demos(p)) } // stemOf is the file name a page link ends in, without its extension // and fragment: the example's name for an example page's link, and "" // for anything else. func stemOf(href string) string { file := href[strings.LastIndex(href, "/")+1:] if i := strings.Index(file, "#"); i >= 0 { return "" } return strings.TrimSuffix(file, ".html") } // sectionOverview is one section's route to the top of its own page: // the first item under it, labelled Overview, which is also the word // the rail's filter matches on, and named for a screen reader by the // section it belongs to. // // The visible label is deliberately the same on every section and the // accessible name deliberately is not. Twelve links reading "Overview" // in one navigation landmark are unambiguous nested under their // headings and ambiguous read out in a list, so the accessible name // carries the section: "Tokens overview". It contains the visible // label, so WCAG 2.2 SC 2.5.3 Label in Name is satisfied without // changing the word a sighted reader sees or types into the filter. func (s *site) sectionOverview(p page, section navSection) navItem { return navItem{ Label: s.say(p.Locale, "gallery.nav.overview"), Href: section.Href, Aria: s.say(p.Locale, "gallery.nav.overview.aria", "section", section.Title), } } // demos is the group at the foot of the rail, every entry opening in a // tab of its own: the five shells, each the layout example's default // frame document, which is the shell laid out whole as the app it // stands for; then the modal route and the demo application. func (s *site) demos(p page) navSection { out := navSection{Title: s.say(p.Locale, "gallery.nav.demos")} for _, shell := range shellNames { out.Items = append(out.Items, navItem{ Label: s.say(p.Locale, "gallery.demos.shell", "shell", shell), Href: s.layoutHref(p.Theme, p.Locale, "layout-"+shell), Blank: true, }) } out.Items = append(out.Items, navItem{Label: s.say(p.Locale, "gallery.demos.modal"), Href: s.modalHref(p.Theme, p.Locale), Blank: true}, navItem{Label: s.say(p.Locale, "gallery.demos.app"), Href: s.demoHref(p.Theme, p.Locale), Blank: true}, ) return out } // indexRows reads the phone index off the rail, so the rows and the // tree cannot list different sections: one row per page kind but the // Overview, which is the index itself, and the Demos section as is. // Rows are plain links, not the tree's disclosures, because shell.js // returns focus to the first nav link whose path is the page just // left, and in the tree that link is inside a closed
that // cannot take focus. anchorID never writes a nav- id, and the kinds // are unique, so the ids cannot collide with anything else on the // page. func indexRows(nav []navSection) ([]indexRow, navSection) { rows := make([]indexRow, 0, len(nav)) for _, s := range nav[:len(nav)-1] { if s.Kind == "overview" { continue } rows = append(rows, indexRow{ID: "nav-" + s.Kind, Title: s.Title, Href: s.Href}) } return rows, nav[len(nav)-1] } // phoneIndex sets the Overview's phone index, Rows and Demos, on the // page being built: the Overview is the shell's index page below // 800px, and every other page a content page with a way back to its // row, so only the Overview carries them. func (s *site) phoneIndex(p *page) { if p.Kind != "overview" { return } p.Rows, p.Demos = indexRows(p.Nav) }