Merge pull request #928 from NoRedInk/bat/breadcrumbs

Add breadcrumbs
This commit is contained in:
Juliano Solanho 2022-05-23 18:07:07 -03:00 committed by GitHub
commit 61a6608dd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 661 additions and 139 deletions

View File

@ -11,6 +11,7 @@
"Nri.Ui.AssetPath",
"Nri.Ui.AssignmentIcon.V2",
"Nri.Ui.Balloon.V1",
"Nri.Ui.BreadCrumbs.V1",
"Nri.Ui.Button.V10",
"Nri.Ui.Checkbox.V5",
"Nri.Ui.ClickableSvg.V2",

View File

@ -0,0 +1,367 @@
module Nri.Ui.BreadCrumbs.V1 exposing
( view, IconStyle(..)
, BreadCrumbs, init
, BreadCrumb, after
, headerId
, toPageTitle, toPageTitleWithSecondaryBreadCrumbs
)
{-| Learn more about 'breadcrumbs' to help a user orient themselves within a site here: <https://www.w3.org/WAI/WCAG21/Techniques/general/G65>.
Wide Viewport (with Circled IconStyle):
Home
🏠 Home > 🟠 Category 1
🏠 > 🟠 Category 1 > 🟣 Sub-Category 2
Narrow Viewport (with Circled IconStyle):
Home
🏠 > 🟠 Category 1
🏠 > 🟠 > 🟣 Sub-Category 2
@docs view, IconStyle
@docs BreadCrumbs, init
@docs BreadCrumb, after
@docs headerId
@docs toPageTitle, toPageTitleWithSecondaryBreadCrumbs
-}
import Accessibility.Styled exposing (..)
import Accessibility.Styled.Aria as Aria
import Accessibility.Styled.Style as Style
import Accessibility.Styled.Widget as Widget
import Css exposing (..)
import Css.Global
import Css.Media as Media
import Html.Styled
import Html.Styled.Attributes as Attributes exposing (css)
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.MediaQuery.V1 as MediaQuery
import Nri.Ui.Svg.V1 as Svg
import Nri.Ui.UiIcon.V1 as UiIcon
{-| -}
type alias BreadCrumb route =
{ icon : Maybe Svg.Svg
, iconStyle : IconStyle
, id : String
, text : String
, route : route
}
{-| -}
type BreadCrumbs route
= BreadCrumbs (List (BreadCrumb route))
{-| -}
init : BreadCrumb route -> BreadCrumbs route
init breadCrumb =
BreadCrumbs [ breadCrumb ]
{-| -}
after : BreadCrumbs route -> BreadCrumb route -> BreadCrumbs route
after (BreadCrumbs previous) new =
BreadCrumbs (new :: previous)
{-| -}
headerId : BreadCrumbs route -> String
headerId (BreadCrumbs list) =
case list of
{ id } :: _ ->
id
_ ->
-- It should be impossible to construct a BreadCrumbs without
-- any elements.
--
""
{-| -}
type IconStyle
= Circled
| Default
{-| Generate an HTML page title using the breadcrumbs,
in the form "Sub-Category | Category | NoRedInk" for breadCrumbs like:
Category > Sub - Category
-}
toPageTitle : BreadCrumbs a -> String
toPageTitle (BreadCrumbs breadcrumbs) =
String.join " | " (List.map .text breadcrumbs ++ [ "NoRedInk" ])
{-| -}
toPageTitleWithSecondaryBreadCrumbs : BreadCrumbs a -> String
toPageTitleWithSecondaryBreadCrumbs (BreadCrumbs breadcrumbs) =
(List.take 1 breadcrumbs |> List.map .text)
++ [ "NoRedInk" ]
|> String.join " | "
{-| -}
view :
{ aTagAttributes : route -> List (Attribute msg)
, isCurrentRoute : route -> Bool
}
-> BreadCrumbs route
-> Html msg
view config (BreadCrumbs breadCrumbs) =
styled nav
[ alignItems center
, displayFlex
, Media.withMedia [ MediaQuery.mobile ] [ marginBottom (px 10) ]
]
[ Widget.label "breadcrumbs" ]
(viewBreadCrumbs config (List.reverse breadCrumbs))
viewBreadCrumbs :
{ aTagAttributes : route -> List (Attribute msg)
, isCurrentRoute : route -> Bool
}
-> List (BreadCrumb route)
-> List (Html msg)
viewBreadCrumbs config breadCrumbs =
let
breadCrumbCount : Int
breadCrumbCount =
List.length breadCrumbs
in
List.indexedMap
(\i ->
viewBreadCrumb config
{ isFirst = i == 0
, isLast = (i + 1) == breadCrumbCount
, isIconOnly =
-- the first breadcrumb should collapse when there
-- are 3 breadcrumbs or more
--
-- Hypothetically, if there were 4 breadcrumbs, then the
-- first 2 breadcrumbs should collapse
(breadCrumbCount - i) > 2
}
)
breadCrumbs
|> List.intersperse (Svg.toHtml arrowRight)
viewBreadCrumb :
{ config
| aTagAttributes : route -> List (Attribute msg)
, isCurrentRoute : route -> Bool
}
-> { isFirst : Bool, isLast : Bool, isIconOnly : Bool }
-> BreadCrumb route
-> Html msg
viewBreadCrumb config iconConfig crumb =
let
isLink =
not (config.isCurrentRoute crumb.route)
linkAttrs =
if isLink then
css
[ hover
[ Css.Global.descendants
[ Css.Global.class circleIconClass
[ backgroundColor Colors.glacier
, borderColor Colors.azureDark
, color Colors.azure
]
]
]
]
:: config.aTagAttributes crumb.route
else
[]
withIconIfPresent viewIcon =
case crumb.icon of
Just icon ->
[ viewIcon iconConfig.isFirst crumb.iconStyle icon
, viewHeadingWithIcon iconConfig crumb.text
]
Nothing ->
[ text crumb.text ]
in
case ( iconConfig.isLast, isLink ) of
( True, False ) ->
pageHeader crumb.id
(withIconIfPresent viewIconForHeading)
( True, True ) ->
pageHeader crumb.id
[ Html.Styled.styled Html.Styled.a
[]
(css commonCss :: linkAttrs)
(withIconIfPresent viewIconForLink)
]
( False, _ ) ->
Html.Styled.styled Html.Styled.a
[ fontWeight normal ]
(css commonCss :: Attributes.id crumb.id :: linkAttrs)
(withIconIfPresent viewIconForLink)
pageHeader : String -> List (Html msg) -> Html msg
pageHeader id =
styled h1
[ fontWeight bold ]
[ Aria.currentPage
, Attributes.id id
, Attributes.tabindex -1
, css commonCss
]
viewIconForHeading : Bool -> IconStyle -> Svg.Svg -> Html msg
viewIconForHeading isFirst iconStyle svg =
case iconStyle of
Circled ->
text ""
Default ->
withoutIconCircle isFirst svg
viewIconForLink : Bool -> IconStyle -> Svg.Svg -> Html msg
viewIconForLink isFirst iconStyle svg =
case iconStyle of
Circled ->
withIconCircle svg
Default ->
withoutIconCircle isFirst svg
viewHeadingWithIcon : { config | isLast : Bool, isIconOnly : Bool } -> String -> Html msg
viewHeadingWithIcon { isIconOnly, isLast } title =
div
(if isIconOnly then
Style.invisible
else if isLast then
[ css [ marginLeft horizontalSpacing ] ]
else
[ css
[ marginLeft horizontalSpacing
, Media.withMedia [ MediaQuery.mobile ]
[ Style.invisibleStyle
]
]
]
)
[ text title
]
commonCss : List Style
commonCss =
[ alignItems center
, displayFlex
, margin zero
, fontSize (px 30)
, Media.withMedia [ MediaQuery.mobile ] [ fontSize (px 25) ]
, Fonts.baseFont
, textDecoration none
, color Colors.navy
]
circleIconClass : String
circleIconClass =
"Nri-BreadCrumb-base-circled-icon"
withIconCircle : Svg.Svg -> Html msg
withIconCircle icon =
styled div
[ borderRadius (pct 50)
, border3 (px 1) solid Colors.azure
, color Colors.azure
, borderBottomWidth (px 2)
, backgroundColor Colors.white
, height largeIconSize
, width largeIconSize
, fontSize (px 16)
, property "transition" "background-color 0.2s, color 0.2s"
, displayFlex
, alignItems center
, justifyContent center
]
[ Attributes.class circleIconClass ]
[ icon
|> Svg.withWidth circledInnerIconSize
|> Svg.withHeight circledInnerIconSize
|> Svg.toHtml
]
withoutIconCircle : Bool -> Svg.Svg -> Html msg
withoutIconCircle isFirst icon =
let
size =
if isFirst then
largeIconSize
else
iconSize
in
icon
|> Svg.withWidth size
|> Svg.withHeight size
|> Svg.withCss [ Css.flexShrink Css.zero ]
|> Svg.toHtml
horizontalSpacing : Css.Px
horizontalSpacing =
Css.px 10
circledInnerIconSize : Css.Px
circledInnerIconSize =
Css.px 25
largeIconSize : Css.Px
largeIconSize =
Css.px 40
iconSize : Css.Px
iconSize =
Css.px 31
arrowRight : Svg.Svg
arrowRight =
UiIcon.arrowRight
|> Svg.withColor Colors.gray75
|> Svg.withHeight (px 15)
|> Svg.withWidth (px 15)
|> Svg.withCss
[ marginRight horizontalSpacing
, marginLeft horizontalSpacing
, flexShrink zero
]

View File

@ -1,6 +1,7 @@
module App exposing (Effect(..), Model, Msg(..), init, perform, subscriptions, update, view)
import Accessibility.Styled as Html exposing (Html)
import Accessibility.Styled.Key as Key
import Browser exposing (Document, UrlRequest(..))
import Browser.Dom
import Browser.Navigation exposing (Key)
@ -20,12 +21,16 @@ import Nri.Ui.Page.V3 as Page
import Nri.Ui.SideNav.V3 as SideNav
import Nri.Ui.Sprite.V1 as Sprite
import Nri.Ui.UiIcon.V1 as UiIcon
import Routes exposing (Route)
import Routes
import Sort.Set as Set
import Task
import Url exposing (Url)
type alias Route =
Routes.Route Examples.State Examples.Msg
type alias Model key =
{ -- Global UI
route : Route
@ -38,11 +43,14 @@ type alias Model key =
init : () -> Url -> key -> ( Model key, Effect )
init () url key =
( { route = Routes.fromLocation url
, previousRoute = Nothing
, moduleStates =
let
moduleStates =
Dict.fromList
(List.map (\example -> ( example.name, example )) Examples.all)
in
( { route = Routes.fromLocation moduleStates url
, previousRoute = Nothing
, moduleStates = moduleStates
, navigationKey = key
, elliePackageDependencies = Ok Dict.empty
}
@ -73,11 +81,15 @@ update action model =
example.update exampleMsg example.state
|> Tuple.mapFirst
(\newState ->
let
newExample =
{ example | state = newState }
in
{ model
| moduleStates =
Dict.insert key
{ example | state = newState }
model.moduleStates
| moduleStates = Dict.insert key newExample model.moduleStates
, route =
Maybe.withDefault model.route
(Routes.updateExample newExample model.route)
}
)
|> Tuple.mapSecond (Cmd.map (UpdateModuleStates key) >> Command)
@ -95,7 +107,7 @@ update action model =
OnUrlChange route ->
( { model
| route = Routes.fromLocation route
| route = Routes.fromLocation model.moduleStates route
, previousRoute = Just model.route
}
, None
@ -190,11 +202,6 @@ subscriptions model =
view : Model key -> Document Msg
view model =
let
findExampleByName name =
Dict.values model.moduleStates
|> List.filter (\m -> m.name == name)
|> List.head
toBody view_ =
List.map Html.toUnstyled
[ view_
@ -202,20 +209,20 @@ view model =
]
in
case model.route of
Routes.Doodad doodad ->
case findExampleByName doodad of
Just example ->
{ title = example.name ++ " in the NoRedInk Style Guide"
, body =
viewExample model example
|> Html.map (UpdateModuleStates example.name)
|> toBody
}
Routes.Doodad example ->
{ title = example.name ++ " in the NoRedInk Style Guide"
, body = viewExample model example |> toBody
}
Nothing ->
{ title = "Not found in the NoRedInk Style Guide"
, body = toBody notFound
}
Routes.CategoryDoodad _ example ->
{ title = example.name ++ " in the NoRedInk Style Guide"
, body = viewExample model example |> toBody
}
Routes.NotFound name ->
{ title = name ++ " was not found in the NoRedInk Style Guide"
, body = toBody notFound
}
Routes.Category category ->
{ title = Category.forDisplay category ++ " Category in the NoRedInk Style Guide"
@ -228,13 +235,11 @@ view model =
}
viewExample : Model key -> Example a msg -> Html msg
viewExample : Model key -> Example a Examples.Msg -> Html Msg
viewExample model example =
Html.div [ css [ maxWidth (Css.px 1400), margin auto ] ]
[ Example.view model.previousRoute
{ packageDependencies = model.elliePackageDependencies }
example
]
Example.view { packageDependencies = model.elliePackageDependencies } example
|> Html.map (UpdateModuleStates example.name)
|> withSideNav model.route
notFound : Html Msg
@ -247,17 +252,18 @@ notFound =
viewAll : Model key -> Html Msg
viewAll model =
withSideNav model.route
[ mainContentHeader "All"
, viewPreviews "all" (Dict.values model.moduleStates)
]
withSideNav model.route <|
viewPreviews "all"
{ navigate = Routes.Doodad >> ChangeRoute
, exampleHref = Routes.Doodad >> Routes.toString
}
(Dict.values model.moduleStates)
viewCategory : Model key -> Category -> Html Msg
viewCategory model category =
withSideNav model.route
[ mainContentHeader (Category.forDisplay category)
, model.moduleStates
(model.moduleStates
|> Dict.values
|> List.filter
(\doodad ->
@ -266,10 +272,13 @@ viewCategory model category =
category
)
|> viewPreviews (Category.forId category)
]
{ navigate = Routes.CategoryDoodad category >> ChangeRoute
, exampleHref = Routes.CategoryDoodad category >> Routes.toString
}
)
withSideNav : Route -> List (Html Msg) -> Html Msg
withSideNav : Route -> Html Msg -> Html Msg
withSideNav currentRoute content =
Html.div
[ css
@ -287,25 +296,28 @@ withSideNav currentRoute content =
, margin2 (px 40) zero
, Css.minHeight (Css.vh 100)
]
, id "maincontent"
, Key.tabbable False
]
[ Html.div [ css [ Css.marginBottom (Css.px 30) ] ]
[ Routes.viewBreadCrumbs currentRoute
]
, content
]
content
]
mainContentHeader : String -> Html msg
mainContentHeader heading =
Heading.h1
[ Heading.customAttr (id "maincontent")
, Heading.customAttr (tabindex -1)
, Heading.css [ marginBottom (px 30) ]
]
[ Html.text heading ]
viewPreviews : String -> List (Example state msg) -> Html Msg
viewPreviews containerId examples =
viewPreviews :
String
->
{ navigate : Example Examples.State Examples.Msg -> Msg
, exampleHref : Example Examples.State Examples.Msg -> String
}
-> List (Example Examples.State Examples.Msg)
-> Html Msg
viewPreviews containerId navConfig examples =
examples
|> List.map (\example -> Example.preview ChangeRoute example)
|> List.map (Example.preview navConfig)
|> Html.div
[ id containerId
, css

View File

@ -1,7 +1,7 @@
module Category exposing
( Category(..)
, fromString
, forDisplay, forId
, forDisplay, forId, forRoute
, all
, sorter
)
@ -10,7 +10,7 @@ module Category exposing
@docs Category
@docs fromString
@docs forDisplay, forId
@docs forDisplay, forId, forRoute
@docs all
@docs sorter
@ -107,6 +107,12 @@ forDisplay category =
"Animations"
{-| -}
forRoute : Category -> String
forRoute =
Debug.toString
{-| -}
sorter : Sorter Category
sorter =

View File

@ -1,6 +1,5 @@
module Example exposing (Example, fullName, preview, view, wrapMsg, wrapState)
import Accessibility.Styled.Aria as Aria
import Accessibility.Styled.Widget as Widget
import Category exposing (Category)
import Css exposing (..)
@ -10,13 +9,9 @@ import Html.Styled.Attributes as Attributes
import Html.Styled.Events as Events
import Html.Styled.Lazy as Lazy
import KeyboardSupport exposing (KeyboardSupport)
import Nri.Ui.ClickableSvg.V2 as ClickableSvg
import Nri.Ui.ClickableText.V3 as ClickableText
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Container.V2 as Container
import Nri.Ui.Heading.V2 as Heading
import Nri.Ui.UiIcon.V1 as UiIcon
import Routes exposing (Route)
type alias Example state msg =
@ -98,13 +93,23 @@ wrapState wrapState_ unwrapState example =
}
preview : (Route -> msg2) -> Example state msg -> Html msg2
preview navigate =
Lazy.lazy (preview_ navigate)
preview :
{ navigate : Example state msg -> msg2
, exampleHref : Example state msg -> String
}
-> Example state msg
-> Html msg2
preview navConfig =
Lazy.lazy (preview_ navConfig)
preview_ : (Route -> msg2) -> Example state msg -> Html msg2
preview_ navigate example =
preview_ :
{ navigate : Example state msg -> msg2
, exampleHref : Example state msg -> String
}
-> Example state msg
-> Html msg2
preview_ { navigate, exampleHref } example =
Container.view
[ Container.gray
, Container.css
@ -114,7 +119,7 @@ preview_ navigate example =
, Css.cursor Css.pointer
]
]
, Container.custom [ Events.onClick (navigate (Routes.Doodad example.name)) ]
, Container.custom [ Events.onClick (navigate example) ]
, Container.html
(ClickableText.link example.name
[ ClickableText.href (exampleHref example)
@ -134,26 +139,17 @@ preview_ navigate example =
]
view : Maybe Route -> EllieLink.Config -> Example state msg -> Html msg
view previousRoute ellieLinkConfig example =
Container.view
[ Container.pillow
, Container.css
[ Css.position Css.relative
, Css.margin (Css.px 10)
, Css.minHeight (Css.calc (Css.vh 100) Css.minus (Css.px 20))
, Css.boxSizing Css.borderBox
]
, Container.html (view_ previousRoute ellieLinkConfig example)
, Container.custom [ Attributes.id (String.replace "." "-" example.name) ]
]
view : EllieLink.Config -> Example state msg -> Html msg
view ellieLinkConfig example =
Html.div [ Attributes.id (String.replace "." "-" example.name) ]
(view_ ellieLinkConfig example)
view_ : Maybe Route -> EllieLink.Config -> Example state msg -> List (Html msg)
view_ previousRoute ellieLinkConfig example =
view_ : EllieLink.Config -> Example state msg -> List (Html msg)
view_ ellieLinkConfig example =
let
navMenu items =
Html.nav [ Widget.label "Example" ]
Html.nav [ Widget.label (fullName example) ]
[ Html.ul
[ Attributes.css
[ margin zero
@ -176,50 +172,20 @@ view_ previousRoute ellieLinkConfig example =
)
]
in
[ Html.header
[ Html.div
[ Attributes.css
[ Css.paddingBottom (Css.px 10)
, Css.marginBottom (Css.px 20)
, Css.borderBottom3 (Css.px 1) Css.solid Colors.gray92
]
]
[ navMenu
[ Heading.h1
[ Heading.custom [ Aria.currentPage ] ]
[ Html.text (fullName example)
]
, docsLink example
, srcLink example
, closeExample previousRoute example
]
[ navMenu [ docsLink example, srcLink example ]
]
, KeyboardSupport.view example.keyboardSupport
, Html.main_ [] (example.view ellieLinkConfig example.state)
, Html.div [] (example.view ellieLinkConfig example.state)
]
closeExample : Maybe Route -> Example state msg -> Html msg
closeExample previousRoute example =
ClickableSvg.link ("Close " ++ example.name ++ " example")
UiIcon.x
[ ClickableSvg.href
(Maybe.withDefault Routes.All previousRoute
|> Routes.toString
)
, ClickableSvg.exactSize 20
, ClickableSvg.css
[ Css.position Css.absolute
, Css.top (Css.px 15)
, Css.right (Css.px 15)
]
]
exampleHref : Example state msg -> String
exampleHref example =
Routes.toString (Routes.Doodad example.name)
docsLink : Example state msg -> Html msg
docsLink example =
let
@ -229,7 +195,6 @@ docsLink example =
in
ClickableText.link "Docs"
[ ClickableText.linkExternal link
, ClickableText.css [ Css.marginLeft (Css.px 20) ]
]

View File

@ -4,6 +4,7 @@ import Example exposing (Example)
import Examples.Accordion as Accordion
import Examples.AssignmentIcon as AssignmentIcon
import Examples.Balloon as Balloon
import Examples.BreadCrumbs as BreadCrumbs
import Examples.Button as Button
import Examples.Checkbox as Checkbox
import Examples.ClickableSvg as ClickableSvg
@ -97,6 +98,25 @@ all =
BalloonState childState ->
Just childState
_ ->
Nothing
)
, BreadCrumbs.example
|> Example.wrapMsg BreadCrumbsMsg
(\msg ->
case msg of
BreadCrumbsMsg childMsg ->
Just childMsg
_ ->
Nothing
)
|> Example.wrapState BreadCrumbsState
(\msg ->
case msg of
BreadCrumbsState childState ->
Just childState
_ ->
Nothing
)
@ -772,6 +792,7 @@ type State
= AccordionState Accordion.State
| AssignmentIconState AssignmentIcon.State
| BalloonState Balloon.State
| BreadCrumbsState BreadCrumbs.State
| ButtonState Button.State
| CheckboxState Checkbox.State
| ClickableSvgState ClickableSvg.State
@ -813,6 +834,7 @@ type Msg
= AccordionMsg Accordion.Msg
| AssignmentIconMsg AssignmentIcon.Msg
| BalloonMsg Balloon.Msg
| BreadCrumbsMsg BreadCrumbs.Msg
| ButtonMsg Button.Msg
| CheckboxMsg Checkbox.Msg
| ClickableSvgMsg ClickableSvg.Msg

View File

@ -0,0 +1,39 @@
module Examples.BreadCrumbs exposing (example, State, Msg)
{-|
@docs example, State, Msg
-}
import Category exposing (Category(..))
import Example exposing (Example)
import Nri.Ui.Text.V6 as Text
{-| -}
type alias State =
{}
{-| -}
type alias Msg =
()
{-| -}
example : Example State Msg
example =
{ name = "BreadCrumbs"
, version = 1
, categories = [ Layout ]
, keyboardSupport = []
, state = {}
, update = \_ m -> ( m, Cmd.none )
, subscriptions = \_ -> Sub.none
, preview = []
, view =
\ellieLinkConfig settings ->
[ Text.mediumBody [ Text.plaintext "🚧 Example coming soon! 🚧" ]
]
}

View File

@ -143,14 +143,14 @@ init =
controlNavAttributes : Control (List ( String, SideNav.NavAttribute ))
controlNavAttributes =
ControlExtra.list
|> ControlExtra.optionalListItem "navLabel"
|> ControlExtra.optionalListItemDefaultChecked "navLabel"
(Control.map
(\val ->
( "SideNav.navLabel \"" ++ val ++ "\""
, SideNav.navLabel val
)
)
(Control.string "Entries")
(Control.string "Example")
)
|> ControlExtra.optionalListItem "navNotMobileCss"
(Control.choice

View File

@ -24,7 +24,6 @@ import Nri.Ui.Tabs.V7 as Tabs exposing (Alignment(..), Tab)
import Nri.Ui.Text.V6 as Text
import Nri.Ui.Tooltip.V3 as Tooltip
import Nri.Ui.UiIcon.V1 as UiIcon
import Routes
import Task
@ -217,8 +216,7 @@ allTabs openTooltipId labelledBy =
|> Svg.toHtml
in
[ Tabs.build { id = First, idString = "tab-0" }
([ Tabs.spaHref <| Routes.toString (Routes.Doodad exampleName)
, Tabs.tabString "1"
([ Tabs.tabString "1"
, Tabs.withTooltip
[ Tooltip.plaintext "Link Example"
, Tooltip.onToggle (ToggleTooltip First)

View File

@ -1,36 +1,64 @@
module Routes exposing (Route(..), fromLocation, toString)
module Routes exposing (Route(..), fromLocation, toString, updateExample, viewBreadCrumbs)
import Accessibility.Styled as Html exposing (Html)
import Category
import Dict exposing (Dict)
import Example exposing (Example)
import Html.Styled.Attributes as Attributes
import Nri.Ui.BreadCrumbs.V1 as BreadCrumbs exposing (BreadCrumb, BreadCrumbs)
import Nri.Ui.Util exposing (dashify)
import Parser exposing ((|.), (|=), Parser)
import Url exposing (Url)
type Route
= Doodad String
type Route state msg
= Doodad (Example state msg)
| Category Category.Category
| CategoryDoodad Category.Category (Example state msg)
| All
| NotFound String
toString : Route -> String
toString : Route state msg -> String
toString route_ =
case route_ of
Doodad exampleName ->
"#/doodad/" ++ exampleName
Doodad example ->
"#/doodad/" ++ example.name
Category c ->
"#/category/" ++ Debug.toString c
"#/category/" ++ Category.forRoute c
CategoryDoodad c example ->
"#/category_doodad/" ++ Category.forRoute c ++ "/" ++ example.name
All ->
"#/"
NotFound unmatchedRoute ->
unmatchedRoute
route : Parser Route
route =
route : Dict String (Example state msg) -> Parser (Route state msg)
route examples =
let
findExample : (Example state msg -> Route state msg) -> String -> Route state msg
findExample toRoute name =
Dict.get name examples
|> Maybe.map toRoute
|> Maybe.withDefault (NotFound name)
in
Parser.oneOf
[ Parser.succeed Category
[ Parser.succeed (\cat -> findExample (CategoryDoodad cat))
|. Parser.token "/category_doodad/"
|= (Parser.getChompedString (Parser.chompWhile ((/=) '/'))
|> Parser.andThen category
)
|. Parser.token "/"
|= restOfPath
, Parser.succeed Category
|. Parser.token "/category/"
|= (restOfPath |> Parser.andThen category)
, Parser.succeed Doodad
, Parser.succeed (findExample Doodad)
|. Parser.token "/doodad/"
|= restOfPath
, Parser.succeed All
@ -52,9 +80,91 @@ category string =
Parser.problem e
fromLocation : Url -> Route
fromLocation location =
updateExample : Example state msg -> Route state msg -> Maybe (Route state msg)
updateExample example route_ =
case route_ of
Doodad _ ->
Just (Doodad example)
CategoryDoodad cat _ ->
Just (CategoryDoodad cat example)
_ ->
Nothing
fromLocation : Dict String (Example state msg) -> Url -> Route state msg
fromLocation examples location =
location.fragment
|> Maybe.withDefault ""
|> Parser.run route
|> Parser.run (route examples)
|> Result.withDefault All
viewBreadCrumbs : Route state msg -> Html msg2
viewBreadCrumbs currentRoute =
breadCrumbs currentRoute
|> Maybe.map
(BreadCrumbs.view
{ aTagAttributes = \r -> [ Attributes.href ("/" ++ toString r) ]
, isCurrentRoute = (==) currentRoute
}
)
|> Maybe.withDefault (Html.text "")
breadCrumbs : Route state msg -> Maybe (BreadCrumbs (Route state msg))
breadCrumbs route_ =
case route_ of
All ->
Just allBreadCrumb
Category category_ ->
Just (categoryCrumb category_)
Doodad example ->
Just
(BreadCrumbs.after allBreadCrumb
(doodadCrumb example)
)
CategoryDoodad category_ example ->
Just
(BreadCrumbs.after (categoryCrumb category_)
(doodadCrumb example)
)
NotFound _ ->
Nothing
allBreadCrumb : BreadCrumbs (Route state msg)
allBreadCrumb =
BreadCrumbs.init
{ icon = Nothing
, iconStyle = BreadCrumbs.Default
, id = "breadcrumbs__all"
, text = "All"
, route = All
}
categoryCrumb : Category.Category -> BreadCrumbs (Route state msg)
categoryCrumb category_ =
BreadCrumbs.after allBreadCrumb
{ icon = Nothing
, iconStyle = BreadCrumbs.Default
, id = "breadcrumbs__" ++ Category.forId category_
, text = Category.forDisplay category_
, route = Category category_
}
doodadCrumb : Example state msg -> BreadCrumb (Route state msg)
doodadCrumb example =
{ icon = Nothing
, iconStyle = BreadCrumbs.Default
, id = "breadcrumbs__" ++ dashify example.name
, text = Example.fullName example
, route = Doodad example
}

View File

@ -1,5 +1,6 @@
module SwitchExampleSpec exposing (suite)
import Examples.Switch exposing (Msg, State, example)
import ProgramTest exposing (..)
import Routes exposing (Route)
import Test exposing (..)
@ -7,9 +8,9 @@ import Test.Html.Selector exposing (..)
import TestApp exposing (app)
route : Route
route : Route State Msg
route =
Routes.Doodad "Switch"
Routes.Doodad example
suite : Test

View File

@ -7,6 +7,7 @@
"Nri.Ui.AssetPath",
"Nri.Ui.AssignmentIcon.V2",
"Nri.Ui.Balloon.V1",
"Nri.Ui.BreadCrumbs.V1",
"Nri.Ui.Button.V10",
"Nri.Ui.Checkbox.V5",
"Nri.Ui.ClickableSvg.V2",