| 1 | package gallery |
| 2 | |
| 3 | import ( |
| 4 | "html/template" |
| 5 | "strings" |
| 6 | |
| 7 | "amadan.net/rastrillo/fiesta" |
| 8 | ) |
| 9 | |
| 10 | // The rail's model: one section per page kind with the entries the |
| 11 | // page anchors, read off the same data the page renders from, so an |
| 12 | // entry pointing at nothing and a section with no way to reach it are |
| 13 | // both impossible. The rail is the same on every page apart from the |
| 14 | // current marks: one list of the whole vocabulary rather than a table |
| 15 | // of contents per page, which TestTheRailIsTheSameOnEveryPage holds |
| 16 | // literally. The phone index is read off the rail, so the rows and |
| 17 | // the tree cannot list different sections. |
| 18 | |
| 19 | // navItem is one line in the rail. Code marks an entry whose label is |
| 20 | // an identifier (an example's name, an icon's slug), drawn in the mono |
| 21 | // face. Blank marks an entry that leaves the page and opens in a new |
| 22 | // tab. Aria is the accessible name where it has to differ from the |
| 23 | // visible label; Terms the synonyms the filter matches beside it. |
| 24 | // Current marks the entry that is the page being rendered: an |
| 25 | // example's own line on its page. |
| 26 | type navItem struct { |
| 27 | Label, Href string |
| 28 | Code, Blank, Current bool |
| 29 | Aria, Terms string |
| 30 | } |
| 31 | |
| 32 | // searchTerms are the words people call an example by that its name |
| 33 | // does not contain, keyed by example name and matched by the filter |
| 34 | // beside the name. Rastrillo keys them by anchor id; Fiesta's rail |
| 35 | // entries are example pages, so the key is the page's stem. |
| 36 | // TestEverySearchTermNamesAnAnchor holds every key to a real example |
| 37 | // and every rail link to its terms. |
| 38 | var searchTerms = map[string]string{ |
| 39 | "field-check": "checkbox toggle switch", |
| 40 | "modal": "dialog popup", |
| 41 | "choice-field": "radio", |
| 42 | "list-grid": "table", |
| 43 | "box": "card panel", |
| 44 | "person": "avatar user", |
| 45 | "button": "button submit cta", |
| 46 | "snippet": "code copy clipboard", |
| 47 | "side-nav": "sidebar navigation sections", |
| 48 | } |
| 49 | |
| 50 | // shellNames are the shells the Demos group opens, in the order the |
| 51 | // layout examples are read: the five whose frame document is an app |
| 52 | // laid out whole. The -page pair members are the content pages of the |
| 53 | // sidebar and console shells and are reached from inside those. |
| 54 | var shellNames = []string{"column", "topbar", "sidebar", "console", "stage"} |
| 55 | |
| 56 | // navSection is one group in the rail: a page of the directory, or |
| 57 | // the Demos. Href is the section's own page and Current says whether |
| 58 | // that page is the one being rendered. |
| 59 | type navSection struct { |
| 60 | Title, Href, Kind string |
| 61 | Current bool |
| 62 | Items []navItem |
| 63 | } |
| 64 | |
| 65 | // indexRow is one row of the phone index: a section's page, by title, |
| 66 | // carrying the id its pages' back controls name. |
| 67 | type indexRow struct{ ID, Title, Href string } |
| 68 | |
| 69 | // localeLink is one entry of the language menu: the locale's code, its |
| 70 | // name in its own language, its direction, this page in that locale, |
| 71 | // and whether it is the page's own. |
| 72 | type localeLink struct { |
| 73 | Code, Name, Dir, Href string |
| 74 | Current bool |
| 75 | } |
| 76 | |
| 77 | // controls is one copy of the switchers: the theme tabs, the scheme |
| 78 | // menu (Fiesta's, lifted from the scheme-menu example, so fiesta.js |
| 79 | // owns the scheme) and the language menu. Every page writes them for |
| 80 | // the bar and, on a phone, for the display settings menu, from this |
| 81 | // one value, so the two cannot differ. |
| 82 | type controls struct { |
| 83 | Themes []link |
| 84 | Scheme template.HTML |
| 85 | Locales []localeLink |
| 86 | LocaleName string |
| 87 | } |
| 88 | |
| 89 | // tokenNav is one entry per token group on the Tokens page, in page |
| 90 | // order: the theme's palette groups, then the structure every theme |
| 91 | // shares. |
| 92 | func (s *site) tokenNav(p page) []navItem { |
| 93 | file := s.href(p.Theme, p.Locale, s.fileOf("tokens")) |
| 94 | var items []navItem |
| 95 | for _, g := range s.tokenGroups(p.Theme) { |
| 96 | items = append(items, navItem{Label: s.say(p.Locale, "gallery.tokens.group."+g.Key), Href: file + "#" + anchorID("tokens", g.Key)}) |
| 97 | } |
| 98 | return items |
| 99 | } |
| 100 | |
| 101 | // iconNav is one entry per icon, by slug, in the order the page draws |
| 102 | // them. |
| 103 | func (s *site) iconNav(p page) []navItem { |
| 104 | file := s.href(p.Theme, p.Locale, s.fileOf("icons")) |
| 105 | var items []navItem |
| 106 | for _, ic := range s.icons { |
| 107 | items = append(items, navItem{Label: ic.Slug, Href: file + "#" + anchorID("icon", ic.Slug), Code: true}) |
| 108 | } |
| 109 | return items |
| 110 | } |
| 111 | |
| 112 | // familyNav is one family's entries: its examples, by name, each the |
| 113 | // link to its own page. It closes over the key because the rail is the |
| 114 | // same on every page: every family's section lists that family's |
| 115 | // examples wherever the reader is standing. |
| 116 | func (s *site) familyNav(key string) func(p page) []navItem { |
| 117 | return func(p page) []navItem { |
| 118 | var items []navItem |
| 119 | for _, f := range s.families { |
| 120 | if f.Key != key { |
| 121 | continue |
| 122 | } |
| 123 | for _, e := range f.Examples { |
| 124 | items = append(items, navItem{Label: e.Name, Href: s.href(p.Theme, p.Locale, e.Name+".html"), Code: true}) |
| 125 | } |
| 126 | } |
| 127 | return items |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | // controls builds the switchers for one page: this page in every |
| 132 | // theme, the scheme menu, and this page in every locale, each language |
| 133 | // named in itself. The links carry no fragment; the script adds the |
| 134 | // reader's place at the click. |
| 135 | func (s *site) controls(p page) controls { |
| 136 | c := controls{Scheme: s.schemeIn(p.Locale), LocaleName: s.say(p.Locale, "gallery.locale.name")} |
| 137 | for _, theme := range fiesta.ThemeNames() { |
| 138 | c.Themes = append(c.Themes, link{Href: s.href(theme, p.Locale, p.File), Label: theme, Current: theme == p.Theme}) |
| 139 | } |
| 140 | for _, code := range Locales { |
| 141 | c.Locales = append(c.Locales, localeLink{ |
| 142 | Code: code, |
| 143 | Name: endonym(code), |
| 144 | Dir: Dir(code), |
| 145 | Href: s.href(p.Theme, code, p.File), |
| 146 | Current: code == p.Locale, |
| 147 | }) |
| 148 | } |
| 149 | return c |
| 150 | } |
| 151 | |
| 152 | // galleryNav is the rail for one page: one section per page kind, in |
| 153 | // table order, each with its overview item first and then the entries |
| 154 | // its page anchors, with the search terms on the entries that have |
| 155 | // them; then the Demos. The section whose kind is the page's is |
| 156 | // Current (on an example page that is its family's), and on an |
| 157 | // example page the example's own entry is Current too. |
| 158 | func (s *site) galleryNav(p page) []navSection { |
| 159 | self := s.href(p.Theme, p.Locale, p.File) |
| 160 | out := make([]navSection, 0, len(s.pageKinds())+1) |
| 161 | for _, pk := range s.pageKinds() { |
| 162 | section := navSection{ |
| 163 | Title: s.titleOf(p.Locale, pk), |
| 164 | Href: s.href(p.Theme, p.Locale, pk.File), |
| 165 | Kind: pk.Kind, |
| 166 | Current: pk.Kind == p.Kind, |
| 167 | } |
| 168 | if pk.Nav != nil { |
| 169 | // The section overview link, first, before the entries. A |
| 170 | // section that lists anything draws its title as a |
| 171 | // <summary>, which discloses rather than navigates, so |
| 172 | // without this the section would have no way to its own |
| 173 | // page. A section with nothing to list is already a plain |
| 174 | // link to its page and needs no second one under it. |
| 175 | section.Items = append([]navItem{s.sectionOverview(p, section)}, pk.Nav(p)...) |
| 176 | for i := 1; i < len(section.Items); i++ { |
| 177 | item := §ion.Items[i] |
| 178 | item.Terms = searchTerms[stemOf(item.Href)] |
| 179 | item.Current = item.Href == self |
| 180 | } |
| 181 | } |
| 182 | out = append(out, section) |
| 183 | } |
| 184 | return append(out, s.demos(p)) |
| 185 | } |
| 186 | |
| 187 | // stemOf is the file name a page link ends in, without its extension |
| 188 | // and fragment: the example's name for an example page's link, and "" |
| 189 | // for anything else. |
| 190 | func stemOf(href string) string { |
| 191 | file := href[strings.LastIndex(href, "/")+1:] |
| 192 | if i := strings.Index(file, "#"); i >= 0 { |
| 193 | return "" |
| 194 | } |
| 195 | return strings.TrimSuffix(file, ".html") |
| 196 | } |
| 197 | |
| 198 | // sectionOverview is one section's route to the top of its own page: |
| 199 | // the first item under it, labelled Overview, which is also the word |
| 200 | // the rail's filter matches on, and named for a screen reader by the |
| 201 | // section it belongs to. |
| 202 | // |
| 203 | // The visible label is deliberately the same on every section and the |
| 204 | // accessible name deliberately is not. Twelve links reading "Overview" |
| 205 | // in one navigation landmark are unambiguous nested under their |
| 206 | // headings and ambiguous read out in a list, so the accessible name |
| 207 | // carries the section: "Tokens overview". It contains the visible |
| 208 | // label, so WCAG 2.2 SC 2.5.3 Label in Name is satisfied without |
| 209 | // changing the word a sighted reader sees or types into the filter. |
| 210 | func (s *site) sectionOverview(p page, section navSection) navItem { |
| 211 | return navItem{ |
| 212 | Label: s.say(p.Locale, "gallery.nav.overview"), |
| 213 | Href: section.Href, |
| 214 | Aria: s.say(p.Locale, "gallery.nav.overview.aria", "section", section.Title), |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | // demos is the group at the foot of the rail, every entry opening in a |
| 219 | // tab of its own: the five shells, each the layout example's default |
| 220 | // frame document, which is the shell laid out whole as the app it |
| 221 | // stands for; then the modal route and the demo application. |
| 222 | func (s *site) demos(p page) navSection { |
| 223 | out := navSection{Title: s.say(p.Locale, "gallery.nav.demos")} |
| 224 | for _, shell := range shellNames { |
| 225 | out.Items = append(out.Items, navItem{ |
| 226 | Label: s.say(p.Locale, "gallery.demos.shell", "shell", shell), |
| 227 | Href: s.layoutHref(p.Theme, p.Locale, "layout-"+shell), |
| 228 | Blank: true, |
| 229 | }) |
| 230 | } |
| 231 | out.Items = append(out.Items, |
| 232 | navItem{Label: s.say(p.Locale, "gallery.demos.modal"), Href: s.modalHref(p.Theme, p.Locale), Blank: true}, |
| 233 | navItem{Label: s.say(p.Locale, "gallery.demos.app"), Href: s.demoHref(p.Theme, p.Locale), Blank: true}, |
| 234 | ) |
| 235 | return out |
| 236 | } |
| 237 | |
| 238 | // indexRows reads the phone index off the rail, so the rows and the |
| 239 | // tree cannot list different sections: one row per page kind but the |
| 240 | // Overview, which is the index itself, and the Demos section as is. |
| 241 | // Rows are plain links, not the tree's disclosures, because shell.js |
| 242 | // returns focus to the first nav link whose path is the page just |
| 243 | // left, and in the tree that link is inside a closed <details> that |
| 244 | // cannot take focus. anchorID never writes a nav- id, and the kinds |
| 245 | // are unique, so the ids cannot collide with anything else on the |
| 246 | // page. |
| 247 | func indexRows(nav []navSection) ([]indexRow, navSection) { |
| 248 | rows := make([]indexRow, 0, len(nav)) |
| 249 | for _, s := range nav[:len(nav)-1] { |
| 250 | if s.Kind == "overview" { |
| 251 | continue |
| 252 | } |
| 253 | rows = append(rows, indexRow{ID: "nav-" + s.Kind, Title: s.Title, Href: s.Href}) |
| 254 | } |
| 255 | return rows, nav[len(nav)-1] |
| 256 | } |
| 257 | |
| 258 | // phoneIndex sets the Overview's phone index, Rows and Demos, on the |
| 259 | // page being built: the Overview is the shell's index page below |
| 260 | // 800px, and every other page a content page with a way back to its |
| 261 | // row, so only the Overview carries them. |
| 262 | func (s *site) phoneIndex(p *page) { |
| 263 | if p.Kind != "overview" { |
| 264 | return |
| 265 | } |
| 266 | p.Rows, p.Demos = indexRows(p.Nav) |
| 267 | } |
| 268 | |