mirror of
https://github.com/NoRedInk/noredink-ui.git
synced 2024-09-21 12:19:03 +03:00
Merge pull request #1487 from NoRedInk/tessa/usage-examples-suggestions
Usage Examples
This commit is contained in:
commit
7480debd7e
@ -19,20 +19,20 @@ import Json.Decode as Decode
|
||||
import Nri.Ui.CssVendorPrefix.V1 as VendorPrefixed
|
||||
import Nri.Ui.FocusRing.V1 as FocusRing
|
||||
import Nri.Ui.Header.V1 as Header
|
||||
import Nri.Ui.Heading.V3 as Heading
|
||||
import Nri.Ui.Html.V3 exposing (viewIf)
|
||||
import Nri.Ui.MediaQuery.V1 exposing (mobile)
|
||||
import Nri.Ui.Page.V3 as Page
|
||||
import Nri.Ui.SideNav.V5 as SideNav
|
||||
import Nri.Ui.Spacing.V1 as Spacing
|
||||
import Nri.Ui.Sprite.V1 as Sprite
|
||||
import Nri.Ui.UiIcon.V1 as UiIcon
|
||||
import Routes
|
||||
import Routes exposing (Route)
|
||||
import Sort.Set as Set
|
||||
import Task
|
||||
import Url exposing (Url)
|
||||
|
||||
|
||||
type alias Route =
|
||||
Routes.Route Examples.State Examples.Msg
|
||||
import UsageExample exposing (UsageExample)
|
||||
import UsageExamples
|
||||
|
||||
|
||||
type alias Model key =
|
||||
@ -40,6 +40,7 @@ type alias Model key =
|
||||
route : Route
|
||||
, previousRoute : Maybe Route
|
||||
, moduleStates : Dict String (Example Examples.State Examples.Msg)
|
||||
, usageExampleStates : Dict String (UsageExample UsageExamples.State UsageExamples.Msg)
|
||||
, isSideNavOpen : Bool
|
||||
, openTooltip : Maybe TooltipId
|
||||
, navigationKey : key
|
||||
@ -50,14 +51,19 @@ type alias Model key =
|
||||
|
||||
init : () -> Url -> key -> ( Model key, Effect )
|
||||
init () url key =
|
||||
let
|
||||
moduleStates =
|
||||
Dict.fromList
|
||||
(List.map (\example -> ( example.name, example )) Examples.all)
|
||||
in
|
||||
( { route = Routes.fromLocation moduleStates url
|
||||
( { route = Routes.fromLocation url
|
||||
, previousRoute = Nothing
|
||||
, moduleStates = moduleStates
|
||||
, moduleStates =
|
||||
Dict.fromList
|
||||
(List.map
|
||||
(\example -> ( Example.routeName example, example ))
|
||||
Examples.all
|
||||
)
|
||||
, usageExampleStates =
|
||||
Dict.fromList
|
||||
(List.map (\example -> ( UsageExample.routeName example, example ))
|
||||
UsageExamples.all
|
||||
)
|
||||
, isSideNavOpen = False
|
||||
, openTooltip = Nothing
|
||||
, navigationKey = key
|
||||
@ -78,6 +84,7 @@ type TooltipId
|
||||
|
||||
type Msg
|
||||
= UpdateModuleStates String Examples.Msg
|
||||
| UpdateUsageExamples String UsageExamples.Msg
|
||||
| OnUrlRequest Browser.UrlRequest
|
||||
| OnUrlChange Url
|
||||
| ChangeRoute Route
|
||||
@ -105,9 +112,6 @@ update action model =
|
||||
in
|
||||
{ model
|
||||
| moduleStates = Dict.insert key newExample model.moduleStates
|
||||
, route =
|
||||
Maybe.withDefault model.route
|
||||
(Routes.updateExample newExample model.route)
|
||||
}
|
||||
)
|
||||
|> Tuple.mapSecond (Cmd.map (UpdateModuleStates key) >> Command)
|
||||
@ -115,6 +119,25 @@ update action model =
|
||||
Nothing ->
|
||||
( model, None )
|
||||
|
||||
UpdateUsageExamples key exampleMsg ->
|
||||
case Dict.get key model.usageExampleStates of
|
||||
Just usageExample ->
|
||||
usageExample.update exampleMsg usageExample.state
|
||||
|> Tuple.mapFirst
|
||||
(\newState ->
|
||||
let
|
||||
newExample =
|
||||
{ usageExample | state = newState }
|
||||
in
|
||||
{ model
|
||||
| usageExampleStates = Dict.insert key newExample model.usageExampleStates
|
||||
}
|
||||
)
|
||||
|> Tuple.mapSecond (Cmd.map (UpdateUsageExamples key) >> Command)
|
||||
|
||||
Nothing ->
|
||||
( model, None )
|
||||
|
||||
OnUrlRequest request ->
|
||||
case request of
|
||||
Internal loc ->
|
||||
@ -126,14 +149,14 @@ update action model =
|
||||
OnUrlChange location ->
|
||||
let
|
||||
route =
|
||||
Routes.fromLocation model.moduleStates location
|
||||
Routes.fromLocation location
|
||||
in
|
||||
( { model
|
||||
| route = route
|
||||
, previousRoute = Just model.route
|
||||
, isSideNavOpen = False
|
||||
}
|
||||
, Maybe.map FocusOn (Routes.headerId route)
|
||||
, Maybe.map FocusOn (Routes.headerId route model.moduleStates model.usageExampleStates)
|
||||
|> Maybe.withDefault None
|
||||
)
|
||||
|
||||
@ -233,13 +256,32 @@ perform navigationKey effect =
|
||||
|
||||
subscriptions : Model key -> Sub Msg
|
||||
subscriptions model =
|
||||
let
|
||||
exampleSubs exampleName =
|
||||
case Dict.get exampleName model.moduleStates of
|
||||
Just example ->
|
||||
Sub.map (UpdateModuleStates exampleName)
|
||||
(example.subscriptions example.state)
|
||||
|
||||
Nothing ->
|
||||
Sub.none
|
||||
in
|
||||
Sub.batch
|
||||
[ case model.route of
|
||||
Routes.Doodad example ->
|
||||
Sub.map (UpdateModuleStates example.name) (example.subscriptions example.state)
|
||||
Routes.Doodad exampleName ->
|
||||
exampleSubs exampleName
|
||||
|
||||
Routes.CategoryDoodad _ example ->
|
||||
Sub.map (UpdateModuleStates example.name) (example.subscriptions example.state)
|
||||
Routes.CategoryDoodad _ exampleName ->
|
||||
exampleSubs exampleName
|
||||
|
||||
Routes.Usage exampleName ->
|
||||
case Dict.get exampleName model.usageExampleStates of
|
||||
Just example ->
|
||||
Sub.map (UpdateUsageExamples exampleName)
|
||||
(example.subscriptions example.state)
|
||||
|
||||
Nothing ->
|
||||
Sub.none
|
||||
|
||||
_ ->
|
||||
Sub.none
|
||||
@ -260,28 +302,49 @@ view model =
|
||||
, Css.Global.body [ Css.margin Css.zero ]
|
||||
]
|
||||
]
|
||||
|
||||
exampleDocument exampleName =
|
||||
case Dict.get exampleName model.moduleStates of
|
||||
Just example ->
|
||||
{ title = example.name ++ " in the NoRedInk Component Catalog"
|
||||
, body = viewExample model example |> toBody
|
||||
}
|
||||
|
||||
Nothing ->
|
||||
{ title =
|
||||
"Component example \""
|
||||
++ Example.fromRouteName exampleName
|
||||
++ "\" was not found in the NoRedInk Component Catalog"
|
||||
, body = toBody notFound
|
||||
}
|
||||
in
|
||||
case model.route of
|
||||
Routes.Doodad example ->
|
||||
{ title = example.name ++ " in the NoRedInk Component Catalog"
|
||||
, body = viewExample model example |> toBody
|
||||
}
|
||||
Routes.Doodad exampleName ->
|
||||
exampleDocument exampleName
|
||||
|
||||
Routes.CategoryDoodad _ example ->
|
||||
{ title = example.name ++ " in the NoRedInk Component Catalog"
|
||||
, body = viewExample model example |> toBody
|
||||
}
|
||||
|
||||
Routes.NotFound name ->
|
||||
{ title = name ++ " was not found in the NoRedInk Component Catalog"
|
||||
, body = toBody notFound
|
||||
}
|
||||
Routes.CategoryDoodad _ exampleName ->
|
||||
exampleDocument exampleName
|
||||
|
||||
Routes.Category category ->
|
||||
{ title = Category.forDisplay category ++ " Category in the NoRedInk Component Catalog"
|
||||
, body = toBody (viewCategory model category)
|
||||
}
|
||||
|
||||
Routes.Usage exampleName ->
|
||||
case Dict.get exampleName model.usageExampleStates of
|
||||
Just example ->
|
||||
{ title = example.name ++ " Usage Example in the NoRedInk Component Catalog"
|
||||
, body = viewUsageExample model example |> toBody
|
||||
}
|
||||
|
||||
Nothing ->
|
||||
{ title =
|
||||
"Usage example \""
|
||||
++ UsageExample.fromRouteName exampleName
|
||||
++ "\" was not found in the NoRedInk Component Catalog"
|
||||
, body = toBody notFound
|
||||
}
|
||||
|
||||
Routes.All ->
|
||||
{ title = "NoRedInk Component Catalog"
|
||||
, body = toBody (viewAll model)
|
||||
@ -295,6 +358,13 @@ viewExample model example =
|
||||
|> viewLayout model [ Example.extraLinks (UpdateModuleStates example.name) example ]
|
||||
|
||||
|
||||
viewUsageExample : Model key -> UsageExample a UsageExamples.Msg -> Html Msg
|
||||
viewUsageExample model example =
|
||||
UsageExample.view example
|
||||
|> Html.map (UpdateUsageExamples (UsageExample.routeName example))
|
||||
|> viewLayout model []
|
||||
|
||||
|
||||
notFound : Html Msg
|
||||
notFound =
|
||||
Page.notFound
|
||||
@ -306,37 +376,54 @@ notFound =
|
||||
viewAll : Model key -> Html Msg
|
||||
viewAll model =
|
||||
viewLayout model [] <|
|
||||
viewPreviews "all"
|
||||
viewExamplePreviews "all"
|
||||
{ swallowEvent = SwallowEvent
|
||||
, navigate = Routes.Doodad >> ChangeRoute
|
||||
, exampleHref = Routes.Doodad >> Routes.toString
|
||||
, navigate = Example.routeName >> Routes.Doodad >> ChangeRoute
|
||||
, exampleHref = Example.routeName >> Routes.Doodad >> Routes.toString
|
||||
}
|
||||
{ swallowEvent = SwallowEvent
|
||||
, navigate = UsageExample.routeName >> Routes.Usage >> ChangeRoute
|
||||
, exampleHref = UsageExample.routeName >> Routes.Usage >> Routes.toString
|
||||
}
|
||||
(Dict.values model.moduleStates)
|
||||
(Dict.values model.usageExampleStates)
|
||||
|
||||
|
||||
viewCategory : Model key -> Category -> Html Msg
|
||||
viewCategory model category =
|
||||
viewLayout model [] <|
|
||||
(model.moduleStates
|
||||
|> Dict.values
|
||||
|> List.filter
|
||||
(\doodad ->
|
||||
let
|
||||
filtered items =
|
||||
List.filter
|
||||
(\item ->
|
||||
Set.memberOf
|
||||
(Set.fromList Category.sorter doodad.categories)
|
||||
(Set.fromList Category.sorter item.categories)
|
||||
category
|
||||
)
|
||||
|> viewPreviews (Category.forId category)
|
||||
{ swallowEvent = SwallowEvent
|
||||
, navigate = Routes.CategoryDoodad category >> ChangeRoute
|
||||
, exampleHref = Routes.CategoryDoodad category >> Routes.toString
|
||||
}
|
||||
)
|
||||
(Dict.values items)
|
||||
in
|
||||
viewLayout model [] <|
|
||||
viewExamplePreviews (Category.forId category)
|
||||
{ swallowEvent = SwallowEvent
|
||||
, navigate = Example.routeName >> Routes.CategoryDoodad category >> ChangeRoute
|
||||
, exampleHref = Example.routeName >> Routes.CategoryDoodad category >> Routes.toString
|
||||
}
|
||||
{ swallowEvent = SwallowEvent
|
||||
, navigate = UsageExample.routeName >> Routes.Usage >> ChangeRoute
|
||||
, exampleHref = UsageExample.routeName >> Routes.Usage >> Routes.toString
|
||||
}
|
||||
(filtered model.moduleStates)
|
||||
(filtered model.usageExampleStates)
|
||||
|
||||
|
||||
viewLayout : Model key -> List (Header.Attribute (Routes.Route Examples.State Examples.Msg) Msg) -> Html Msg -> Html Msg
|
||||
viewLayout : Model key -> List (Header.Attribute Route Msg) -> Html Msg -> Html Msg
|
||||
viewLayout model headerExtras content =
|
||||
Html.div []
|
||||
[ Html.header [] [ Routes.viewHeader model.route headerExtras ]
|
||||
[ Html.header []
|
||||
[ Routes.viewHeader model.route
|
||||
model.moduleStates
|
||||
model.usageExampleStates
|
||||
headerExtras
|
||||
]
|
||||
, Html.div
|
||||
[ css
|
||||
[ displayFlex
|
||||
@ -359,27 +446,47 @@ viewLayout model headerExtras content =
|
||||
]
|
||||
|
||||
|
||||
viewPreviews :
|
||||
viewExamplePreviews :
|
||||
String
|
||||
->
|
||||
{ swallowEvent : Msg
|
||||
, navigate : Example Examples.State Examples.Msg -> Msg
|
||||
, exampleHref : Example Examples.State Examples.Msg -> String
|
||||
}
|
||||
->
|
||||
{ swallowEvent : Msg
|
||||
, navigate : UsageExample UsageExamples.State UsageExamples.Msg -> Msg
|
||||
, exampleHref : UsageExample UsageExamples.State UsageExamples.Msg -> String
|
||||
}
|
||||
-> List (Example Examples.State Examples.Msg)
|
||||
-> List (UsageExample UsageExamples.State UsageExamples.Msg)
|
||||
-> Html Msg
|
||||
viewPreviews containerId navConfig examples =
|
||||
examples
|
||||
|> List.map (Example.preview navConfig)
|
||||
|> Html.div
|
||||
[ id containerId
|
||||
, css
|
||||
[ Css.displayFlex
|
||||
, Css.flexWrap Css.wrap
|
||||
, Css.property "row-gap" (.value Spacing.verticalSpacerPx)
|
||||
, Css.property "column-gap" (.value Spacing.horizontalSpacerPx)
|
||||
]
|
||||
viewExamplePreviews containerId exampleNavConfig usageNavConfig examples usageExamples =
|
||||
Html.div [ id containerId ]
|
||||
[ Heading.h2 [ Heading.plaintext "Components" ]
|
||||
, examplesContainer (List.map (Example.preview exampleNavConfig) examples)
|
||||
, viewIf
|
||||
(\_ ->
|
||||
Heading.h2
|
||||
[ Heading.plaintext "Usage Examples"
|
||||
, Heading.css [ Css.marginTop (Css.px 30) ]
|
||||
]
|
||||
)
|
||||
(List.length usageExamples > 0)
|
||||
, examplesContainer (List.map (UsageExample.preview usageNavConfig) usageExamples)
|
||||
]
|
||||
|
||||
|
||||
examplesContainer : List (Html msg) -> Html msg
|
||||
examplesContainer =
|
||||
Html.div
|
||||
[ css
|
||||
[ Css.displayFlex
|
||||
, Css.flexWrap Css.wrap
|
||||
, Css.property "row-gap" (.value Spacing.verticalSpacerPx)
|
||||
, Css.property "column-gap" (.value Spacing.horizontalSpacerPx)
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
navigation : Model key -> Html Msg
|
||||
@ -393,7 +500,8 @@ navigation { moduleStates, route, isSideNavOpen, openTooltip } =
|
||||
|> List.map
|
||||
(\example ->
|
||||
SideNav.entry example.name
|
||||
[ SideNav.href (Routes.CategoryDoodad category example)
|
||||
[ SideNav.href
|
||||
(Routes.CategoryDoodad category (Example.routeName example))
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
module Example exposing (Example, extraLinks, fullName, preview, view, wrapMsg, wrapState)
|
||||
module Example exposing (Example, extraLinks, fromRouteName, fullName, preview, routeName, view, wrapMsg, wrapState)
|
||||
|
||||
import Accessibility.Styled.Aria as Aria
|
||||
import Category exposing (Category)
|
||||
@ -39,6 +39,16 @@ fullName example =
|
||||
"Nri.Ui." ++ example.name ++ ".V" ++ String.fromInt example.version
|
||||
|
||||
|
||||
routeName : { example | name : String } -> String
|
||||
routeName example =
|
||||
String.replace " " "-" example.name
|
||||
|
||||
|
||||
fromRouteName : String -> String
|
||||
fromRouteName name =
|
||||
String.replace "-" " " name
|
||||
|
||||
|
||||
wrapMsg :
|
||||
(msg -> msg2)
|
||||
-> (msg2 -> Maybe msg)
|
||||
|
@ -77,7 +77,7 @@ example =
|
||||
, about =
|
||||
[ let
|
||||
url =
|
||||
Routes.toString <| Routes.Doodad RadioButtonDotlessExample.example
|
||||
Routes.exampleHref RadioButtonDotlessExample.example
|
||||
in
|
||||
Message.view
|
||||
[ Message.markdown <| "Looking for a group of buttons where only one button is selectable at a time? Check out [RadioButtonDotless](" ++ url ++ ")"
|
||||
|
@ -59,7 +59,7 @@ example =
|
||||
, about =
|
||||
[ let
|
||||
url =
|
||||
Routes.toString <| Routes.Doodad RadioButtonDotlessExample.example
|
||||
Routes.exampleHref RadioButtonDotlessExample.example
|
||||
in
|
||||
Message.view
|
||||
[ Message.markdown <| "Looking for radio button that's styled more like a button?<br />Check out [RadioButtonDotless](" ++ url ++ ")"
|
||||
|
@ -18,7 +18,6 @@ import Debug.Control.View as ControlView
|
||||
import EllieLink
|
||||
import Example exposing (Example)
|
||||
import Html.Styled.Attributes exposing (css, href, id)
|
||||
import Html.Styled.Events as Events
|
||||
import KeyboardSupport exposing (Key(..))
|
||||
import Markdown
|
||||
import Nri.Ui.ClickableSvg.V2 as ClickableSvg
|
||||
@ -29,6 +28,8 @@ import Nri.Ui.Svg.V1 as Svg
|
||||
import Nri.Ui.Table.V7 as Table
|
||||
import Nri.Ui.Tooltip.V3 as Tooltip
|
||||
import Nri.Ui.UiIcon.V1 as UiIcon
|
||||
import Routes
|
||||
import UsageExamples.ClickableCardWithTooltip
|
||||
|
||||
|
||||
version : Int
|
||||
@ -94,7 +95,6 @@ example =
|
||||
type alias State =
|
||||
{ openTooltip : Maybe TooltipId
|
||||
, staticExampleSettings : Control (List ( String, Tooltip.Attribute Never ))
|
||||
, disclosureModel : { parentClicks : Int }
|
||||
, pageSettings : Control PageSettings
|
||||
}
|
||||
|
||||
@ -103,7 +103,6 @@ init : State
|
||||
init =
|
||||
{ openTooltip = Nothing
|
||||
, staticExampleSettings = initStaticExampleSettings
|
||||
, disclosureModel = { parentClicks = 0 }
|
||||
, pageSettings =
|
||||
Control.record PageSettings
|
||||
|> Control.field "backgroundColor"
|
||||
@ -132,11 +131,6 @@ type Msg
|
||||
| SetControl (Control (List ( String, Tooltip.Attribute Never )))
|
||||
| UpdatePageSettings (Control PageSettings)
|
||||
| Log String
|
||||
| DisclosureMsg DisclosureMsg
|
||||
|
||||
|
||||
type DisclosureMsg
|
||||
= ParentClick
|
||||
|
||||
|
||||
update : Msg -> State -> ( State, Cmd Msg )
|
||||
@ -158,9 +152,6 @@ update msg model =
|
||||
Log message ->
|
||||
( Debug.log "Tooltip Log:" |> always model, Cmd.none )
|
||||
|
||||
DisclosureMsg ParentClick ->
|
||||
( { model | disclosureModel = { parentClicks = model.disclosureModel.parentClicks + 1 } }, Cmd.none )
|
||||
|
||||
|
||||
view : EllieLink.Config -> State -> List (Html Msg)
|
||||
view ellieLinkConfig model =
|
||||
@ -244,12 +235,14 @@ Use when all of the following are true:
|
||||
This type may contain interactive elements such as links.
|
||||
"""
|
||||
, description =
|
||||
"""
|
||||
Sometimes a tooltip trigger doesn't have any functionality itself outside of revealing information.
|
||||
|
||||
This behavior is analogous to disclosure behavior, except that it's presented different visually. (For more information, please read [Sarah Higley's "Tooltips in the time of WCAG 2.1" post](https://sarahmhigley.com/writing/tooltips-in-wcag-21).)
|
||||
"""
|
||||
, example = viewDisclosureToolip model.openTooltip model.disclosureModel
|
||||
[ "Sometimes a tooltip trigger doesn't have any functionality itself outside of revealing information.\n\n"
|
||||
, "This behavior is analogous to disclosure behavior, except that it's presented different visually. (For more information, please read [Sarah Higley's \"Tooltips in the time of WCAG 2.1\" post](https://sarahmhigley.com/writing/tooltips-in-wcag-21).)\n\n"
|
||||
, "Are you trying to use this tooltip type inside a clickable card? Check out [the Clickable Card with Tooltip example]("
|
||||
, Routes.usageExampleHref UsageExamples.ClickableCardWithTooltip.example
|
||||
, ")."
|
||||
]
|
||||
|> String.join ""
|
||||
, example = viewDisclosureToolip model.openTooltip
|
||||
, tooltipId = Disclosure
|
||||
}
|
||||
, { name = "Tooltip.viewToggleTip"
|
||||
@ -262,9 +255,12 @@ This type may contain interactive elements such as links.
|
||||
|
||||
"""
|
||||
, description =
|
||||
"""
|
||||
This is a helper for using Tooltip.disclosure with a "?" icon because it is a commonly used UI pattern. We use this helper when we want to show more information about an element but we don't want the element itself to have its own tooltip. The "?" icon typically appears visually adjacent to the element it reveals information about.
|
||||
"""
|
||||
[ "This is a helper for using Tooltip.disclosure with a \"?\" icon because it is a commonly used UI pattern. We use this helper when we want to show more information about an element but we don't want the element itself to have its own tooltip. The \"?\" icon typically appears visually adjacent to the element it reveals information about.\n\n"
|
||||
, "Are you trying to use this tooltip type inside a clickable card? Check out [the Clickable Card with Tooltip example]("
|
||||
, Routes.usageExampleHref UsageExamples.ClickableCardWithTooltip.example
|
||||
, ")."
|
||||
]
|
||||
|> String.join ""
|
||||
, example = viewToggleTip model.openTooltip
|
||||
, tooltipId = LearnMore
|
||||
}
|
||||
@ -315,8 +311,8 @@ viewAuxillaryDescriptionToolip openTooltip =
|
||||
]
|
||||
|
||||
|
||||
viewDisclosureToolip : Maybe TooltipId -> { parentClicks : Int } -> Html Msg
|
||||
viewDisclosureToolip openTooltip { parentClicks } =
|
||||
viewDisclosureToolip : Maybe TooltipId -> Html Msg
|
||||
viewDisclosureToolip openTooltip =
|
||||
let
|
||||
triggerId =
|
||||
"tooltip__disclosure-trigger"
|
||||
@ -324,36 +320,29 @@ viewDisclosureToolip openTooltip { parentClicks } =
|
||||
lastId =
|
||||
"tooltip__disclosure-what-is-mastery"
|
||||
in
|
||||
Html.button
|
||||
[ css [ Css.padding (Css.px 40) ]
|
||||
, Events.onClick (DisclosureMsg ParentClick)
|
||||
, id "parent-button"
|
||||
]
|
||||
[ Tooltip.view
|
||||
{ id = "tooltip__disclosure"
|
||||
, trigger =
|
||||
\eventHandlers ->
|
||||
ClickableSvg.button "Previously mastered"
|
||||
(Svg.withColor Colors.green UiIcon.starFilled)
|
||||
[ ClickableSvg.custom eventHandlers
|
||||
, ClickableSvg.id triggerId
|
||||
]
|
||||
}
|
||||
[ Tooltip.html
|
||||
[ Html.text "You mastered this skill in a previous year! Way to go! "
|
||||
, Html.a
|
||||
[ id lastId
|
||||
, href "https://noredink.zendesk.com/hc/en-us/articles/203022319-What-is-mastery-"
|
||||
Tooltip.view
|
||||
{ id = "tooltip__disclosure"
|
||||
, trigger =
|
||||
\eventHandlers ->
|
||||
ClickableSvg.button "Previously mastered"
|
||||
(Svg.withColor Colors.green UiIcon.starFilled)
|
||||
[ ClickableSvg.custom eventHandlers
|
||||
, ClickableSvg.id triggerId
|
||||
]
|
||||
[ Html.text "Learn more about NoRedInk Mastery" ]
|
||||
}
|
||||
[ Tooltip.html
|
||||
[ Html.text "You mastered this skill in a previous year! Way to go! "
|
||||
, Html.a
|
||||
[ id lastId
|
||||
, href "https://noredink.zendesk.com/hc/en-us/articles/203022319-What-is-mastery-"
|
||||
]
|
||||
, Tooltip.disclosure { triggerId = triggerId, lastId = Just lastId }
|
||||
, Tooltip.onToggle (ToggleTooltip Disclosure)
|
||||
, Tooltip.open (openTooltip == Just Disclosure)
|
||||
, Tooltip.smallPadding
|
||||
, Tooltip.alignEndForMobile (Css.px 148)
|
||||
[ Html.text "Learn more about NoRedInk Mastery" ]
|
||||
]
|
||||
, Html.div [ id "parent-button-clicks" ] [ Html.text ("Parent Clicks: " ++ String.fromInt parentClicks) ]
|
||||
, Tooltip.disclosure { triggerId = triggerId, lastId = Just lastId }
|
||||
, Tooltip.onToggle (ToggleTooltip Disclosure)
|
||||
, Tooltip.open (openTooltip == Just Disclosure)
|
||||
, Tooltip.smallPadding
|
||||
, Tooltip.alignEndForMobile (Css.px 148)
|
||||
]
|
||||
|
||||
|
||||
|
@ -1,4 +1,18 @@
|
||||
module Routes exposing (Route(..), fromLocation, headerId, toString, updateExample, viewHeader)
|
||||
module Routes exposing
|
||||
( Route(..), toString, fromLocation
|
||||
, viewHeader, headerId
|
||||
, exampleRoute
|
||||
, exampleHref, usageExampleHref
|
||||
)
|
||||
|
||||
{-|
|
||||
|
||||
@docs Route, toString, fromLocation
|
||||
@docs viewHeader, headerId
|
||||
@docs exampleRoute
|
||||
@docs exampleHref, usageExampleHref
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled as Html exposing (Html)
|
||||
import Category
|
||||
@ -11,46 +25,40 @@ import Nri.Ui.Header.V1 as Header
|
||||
import Nri.Ui.Html.Attributes.V2 exposing (safeIdWithPrefix)
|
||||
import Parser exposing ((|.), (|=), Parser)
|
||||
import Url exposing (Url)
|
||||
import UsageExample exposing (UsageExample)
|
||||
|
||||
|
||||
type Route state msg
|
||||
= Doodad (Example state msg)
|
||||
type Route
|
||||
= Doodad String
|
||||
| Category Category.Category
|
||||
| CategoryDoodad Category.Category (Example state msg)
|
||||
| CategoryDoodad Category.Category String
|
||||
| Usage String
|
||||
| All
|
||||
| NotFound String
|
||||
|
||||
|
||||
toString : Route state msg -> String
|
||||
toString : Route -> String
|
||||
toString route_ =
|
||||
case route_ of
|
||||
Doodad example ->
|
||||
"#/doodad/" ++ example.name
|
||||
Doodad exampleName ->
|
||||
"#/doodad/" ++ exampleName
|
||||
|
||||
Category c ->
|
||||
"#/category/" ++ Category.forRoute c
|
||||
|
||||
CategoryDoodad c example ->
|
||||
"#/category_doodad/" ++ Category.forRoute c ++ "/" ++ example.name
|
||||
CategoryDoodad c exampleName ->
|
||||
"#/category_doodad/" ++ Category.forRoute c ++ "/" ++ exampleName
|
||||
|
||||
Usage exampleName ->
|
||||
"#/usage_example/" ++ exampleName
|
||||
|
||||
All ->
|
||||
"#/"
|
||||
|
||||
NotFound unmatchedRoute ->
|
||||
unmatchedRoute
|
||||
|
||||
|
||||
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
|
||||
route : Parser Route
|
||||
route =
|
||||
Parser.oneOf
|
||||
[ Parser.succeed (\cat -> findExample (CategoryDoodad cat))
|
||||
[ Parser.succeed CategoryDoodad
|
||||
|. Parser.token "/category_doodad/"
|
||||
|= (Parser.getChompedString (Parser.chompWhile ((/=) '/'))
|
||||
|> Parser.andThen category
|
||||
@ -60,9 +68,12 @@ route examples =
|
||||
, Parser.succeed Category
|
||||
|. Parser.token "/category/"
|
||||
|= (restOfPath |> Parser.andThen category)
|
||||
, Parser.succeed (findExample Doodad)
|
||||
, Parser.succeed Doodad
|
||||
|. Parser.token "/doodad/"
|
||||
|= restOfPath
|
||||
, Parser.succeed Usage
|
||||
|. Parser.token "/usage_example/"
|
||||
|= restOfPath
|
||||
, Parser.succeed All
|
||||
]
|
||||
|
||||
@ -82,30 +93,22 @@ category string =
|
||||
Parser.problem e
|
||||
|
||||
|
||||
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 =
|
||||
fromLocation : Url -> Route
|
||||
fromLocation location =
|
||||
location.fragment
|
||||
|> Maybe.withDefault ""
|
||||
|> Parser.run (route examples)
|
||||
|> Parser.run route
|
||||
|> Result.withDefault All
|
||||
|
||||
|
||||
viewHeader : Route state msg -> List (Header.Attribute (Route state msg) msg2) -> Html msg2
|
||||
viewHeader currentRoute extraContent =
|
||||
breadCrumbs currentRoute
|
||||
viewHeader :
|
||||
Route
|
||||
-> Dict String (Example state msg)
|
||||
-> Dict String (UsageExample usageState usageMsg)
|
||||
-> List (Header.Attribute Route msg2)
|
||||
-> Html msg2
|
||||
viewHeader currentRoute examples usageExamples extraContent =
|
||||
breadCrumbs currentRoute examples usageExamples
|
||||
|> Maybe.map
|
||||
(\crumbs ->
|
||||
Header.view
|
||||
@ -121,13 +124,21 @@ viewHeader currentRoute extraContent =
|
||||
|> Maybe.withDefault (Html.text "")
|
||||
|
||||
|
||||
headerId : Route state msg -> Maybe String
|
||||
headerId route_ =
|
||||
Maybe.map BreadCrumbs.headerId (breadCrumbs route_)
|
||||
headerId :
|
||||
Route
|
||||
-> Dict String (Example state msg)
|
||||
-> Dict String (UsageExample usageState usageMsg)
|
||||
-> Maybe String
|
||||
headerId route_ examples usageExamples =
|
||||
Maybe.map BreadCrumbs.headerId (breadCrumbs route_ examples usageExamples)
|
||||
|
||||
|
||||
breadCrumbs : Route state msg -> Maybe (BreadCrumbs (Route state msg))
|
||||
breadCrumbs route_ =
|
||||
breadCrumbs :
|
||||
Route
|
||||
-> Dict String (Example state msg)
|
||||
-> Dict String (UsageExample usageState usageMsg)
|
||||
-> Maybe (BreadCrumbs Route)
|
||||
breadCrumbs route_ examples usageExamples =
|
||||
case route_ of
|
||||
All ->
|
||||
Just allBreadCrumb
|
||||
@ -135,17 +146,21 @@ breadCrumbs route_ =
|
||||
Category category_ ->
|
||||
Just (categoryCrumb category_)
|
||||
|
||||
Doodad example ->
|
||||
Just (doodadCrumb allBreadCrumb example)
|
||||
Doodad exampleName ->
|
||||
Maybe.map (doodadCrumb allBreadCrumb) (Dict.get exampleName examples)
|
||||
|
||||
CategoryDoodad category_ example ->
|
||||
Just (doodadCrumb (categoryCrumb category_) example)
|
||||
CategoryDoodad category_ exampleName ->
|
||||
Maybe.map
|
||||
(\example ->
|
||||
doodadCrumb (categoryCrumb category_) example
|
||||
)
|
||||
(Dict.get exampleName examples)
|
||||
|
||||
NotFound _ ->
|
||||
Nothing
|
||||
Usage exampleName ->
|
||||
Maybe.map usageExampleCrumb (Dict.get exampleName usageExamples)
|
||||
|
||||
|
||||
allBreadCrumb : BreadCrumbs (Route state msg)
|
||||
allBreadCrumb : BreadCrumbs Route
|
||||
allBreadCrumb =
|
||||
BreadCrumbs.init
|
||||
{ id = "breadcrumbs__all"
|
||||
@ -155,7 +170,7 @@ allBreadCrumb =
|
||||
[]
|
||||
|
||||
|
||||
categoryCrumb : Category.Category -> BreadCrumbs (Route state msg)
|
||||
categoryCrumb : Category.Category -> BreadCrumbs Route
|
||||
categoryCrumb category_ =
|
||||
BreadCrumbs.after allBreadCrumb
|
||||
{ id = "breadcrumbs__" ++ Category.forId category_
|
||||
@ -165,11 +180,41 @@ categoryCrumb category_ =
|
||||
[]
|
||||
|
||||
|
||||
doodadCrumb : BreadCrumbs (Route state msg) -> Example state msg -> BreadCrumbs (Route state msg)
|
||||
doodadCrumb : BreadCrumbs Route -> Example state msg -> BreadCrumbs Route
|
||||
doodadCrumb previous example =
|
||||
BreadCrumbs.after previous
|
||||
{ id = safeIdWithPrefix "breadcrumbs" example.name
|
||||
, text = Example.fullName example
|
||||
, route = Doodad example
|
||||
, route = Doodad (Example.routeName example)
|
||||
}
|
||||
[]
|
||||
|
||||
|
||||
usageExampleCrumb : UsageExample a b -> BreadCrumbs Route
|
||||
usageExampleCrumb example =
|
||||
BreadCrumbs.after allBreadCrumb
|
||||
{ id = safeIdWithPrefix "breadcrumbs" example.name
|
||||
, text = UsageExample.fullName example
|
||||
, route = Usage (UsageExample.routeName example)
|
||||
}
|
||||
[]
|
||||
|
||||
|
||||
exampleRoute : Example a b -> Route
|
||||
exampleRoute example =
|
||||
Doodad (Example.routeName example)
|
||||
|
||||
|
||||
exampleHref : Example a b -> String
|
||||
exampleHref =
|
||||
exampleRoute >> toString
|
||||
|
||||
|
||||
usageExampleRoute : UsageExample a b -> Route
|
||||
usageExampleRoute example =
|
||||
Usage (UsageExample.routeName example)
|
||||
|
||||
|
||||
usageExampleHref : UsageExample a b -> String
|
||||
usageExampleHref =
|
||||
usageExampleRoute >> toString
|
||||
|
173
component-catalog/src/UsageExample.elm
Normal file
173
component-catalog/src/UsageExample.elm
Normal file
@ -0,0 +1,173 @@
|
||||
module UsageExample exposing (UsageExample, fromRouteName, fullName, preview, routeName, view, wrapMsg, wrapState)
|
||||
|
||||
import Category exposing (Category)
|
||||
import Css
|
||||
import Css.Media exposing (withMedia)
|
||||
import EventExtras
|
||||
import ExampleSection
|
||||
import Html.Styled as Html exposing (Html)
|
||||
import Html.Styled.Attributes as Attributes
|
||||
import Html.Styled.Events as Events
|
||||
import Html.Styled.Lazy as Lazy
|
||||
import Nri.Ui.ClickableText.V3 as ClickableText
|
||||
import Nri.Ui.Colors.V1 as Colors
|
||||
import Nri.Ui.Container.V2 as Container
|
||||
import Nri.Ui.MediaQuery.V1 exposing (mobile)
|
||||
import Nri.Ui.Text.V6 as Text
|
||||
|
||||
|
||||
type alias UsageExample state msg =
|
||||
{ name : String
|
||||
, state : state
|
||||
, update : msg -> state -> ( state, Cmd msg )
|
||||
, subscriptions : state -> Sub msg
|
||||
, view : state -> List (Html msg)
|
||||
, about : List (Html Never)
|
||||
, categories : List Category
|
||||
}
|
||||
|
||||
|
||||
fullName : { example | name : String } -> String
|
||||
fullName example =
|
||||
example.name
|
||||
|
||||
|
||||
routeName : { example | name : String } -> String
|
||||
routeName example =
|
||||
String.replace " " "-" example.name
|
||||
|
||||
|
||||
fromRouteName : String -> String
|
||||
fromRouteName name =
|
||||
String.replace "-" " " name
|
||||
|
||||
|
||||
wrapMsg :
|
||||
(msg -> msg2)
|
||||
-> (msg2 -> Maybe msg)
|
||||
-> UsageExample state msg
|
||||
-> UsageExample state msg2
|
||||
wrapMsg wrapMsg_ unwrapMsg example =
|
||||
{ name = example.name
|
||||
, state = example.state
|
||||
, update =
|
||||
\msg2 state ->
|
||||
case unwrapMsg msg2 of
|
||||
Just msg ->
|
||||
example.update msg state
|
||||
|> Tuple.mapSecond (Cmd.map wrapMsg_)
|
||||
|
||||
Nothing ->
|
||||
( state, Cmd.none )
|
||||
, subscriptions = \state -> Sub.map wrapMsg_ (example.subscriptions state)
|
||||
, view =
|
||||
\state ->
|
||||
List.map (Html.map wrapMsg_)
|
||||
(example.view state)
|
||||
, about = example.about
|
||||
, categories = example.categories
|
||||
}
|
||||
|
||||
|
||||
wrapState :
|
||||
(state -> state2)
|
||||
-> (state2 -> Maybe state)
|
||||
-> UsageExample state msg
|
||||
-> UsageExample state2 msg
|
||||
wrapState wrapState_ unwrapState example =
|
||||
{ name = example.name
|
||||
, state = wrapState_ example.state
|
||||
, update =
|
||||
\msg state2 ->
|
||||
case unwrapState state2 of
|
||||
Just state ->
|
||||
example.update msg state
|
||||
|> Tuple.mapFirst wrapState_
|
||||
|
||||
Nothing ->
|
||||
( state2, Cmd.none )
|
||||
, subscriptions =
|
||||
unwrapState
|
||||
>> Maybe.map example.subscriptions
|
||||
>> Maybe.withDefault Sub.none
|
||||
, view =
|
||||
\state ->
|
||||
Maybe.map example.view (unwrapState state)
|
||||
|> Maybe.withDefault []
|
||||
, about = example.about
|
||||
, categories = example.categories
|
||||
}
|
||||
|
||||
|
||||
preview :
|
||||
{ swallowEvent : msg2
|
||||
, navigate : UsageExample state msg -> msg2
|
||||
, exampleHref : UsageExample state msg -> String
|
||||
}
|
||||
-> UsageExample state msg
|
||||
-> Html msg2
|
||||
preview navConfig =
|
||||
Lazy.lazy (preview_ navConfig)
|
||||
|
||||
|
||||
preview_ :
|
||||
{ swallowEvent : msg2
|
||||
, navigate : UsageExample state msg -> msg2
|
||||
, exampleHref : UsageExample state msg -> String
|
||||
}
|
||||
-> UsageExample state msg
|
||||
-> Html msg2
|
||||
preview_ { swallowEvent, navigate, exampleHref } example =
|
||||
Container.view
|
||||
[ Container.gray
|
||||
, Container.css
|
||||
[ Css.flexBasis (Css.px 200)
|
||||
, Css.flexShrink Css.zero
|
||||
, Css.hover
|
||||
[ Css.backgroundColor Colors.glacier
|
||||
, Css.cursor Css.pointer
|
||||
]
|
||||
]
|
||||
, Container.custom [ Events.onClick (navigate example) ]
|
||||
, Container.html
|
||||
[ Html.span [ EventExtras.onClickStopPropagation swallowEvent ]
|
||||
[ ClickableText.link example.name
|
||||
[ ClickableText.href (exampleHref example)
|
||||
, ClickableText.nriDescription "usage-example-link"
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
view : UsageExample state msg -> Html msg
|
||||
view example =
|
||||
Html.div [ Attributes.id (String.replace " " "-" example.name) ]
|
||||
(view_ example)
|
||||
|
||||
|
||||
view_ : UsageExample state msg -> List (Html msg)
|
||||
view_ example =
|
||||
[ Html.div
|
||||
[ Attributes.css
|
||||
[ Css.displayFlex
|
||||
, Css.alignItems Css.stretch
|
||||
, Css.flexWrap Css.wrap
|
||||
, Css.property "gap" "10px"
|
||||
, withMedia [ mobile ] [ Css.flexDirection Css.column, Css.alignItems Css.stretch ]
|
||||
]
|
||||
]
|
||||
[ ExampleSection.sectionWithCss "About"
|
||||
[ Css.flex (Css.int 1) ]
|
||||
viewAbout
|
||||
example.about
|
||||
]
|
||||
, Html.div [ Attributes.css [ Css.marginBottom (Css.px 200) ] ]
|
||||
(example.view example.state)
|
||||
]
|
||||
|
||||
|
||||
viewAbout : List (Html Never) -> Html msg
|
||||
viewAbout about =
|
||||
Text.mediumBody [ Text.html about ]
|
||||
|> Html.map never
|
58
component-catalog/src/UsageExamples.elm
Normal file
58
component-catalog/src/UsageExamples.elm
Normal file
@ -0,0 +1,58 @@
|
||||
module UsageExamples exposing (Msg, State, all)
|
||||
|
||||
import UsageExample exposing (UsageExample)
|
||||
import UsageExamples.ClickableCardWithTooltip as ClickableCardWithTooltip
|
||||
import UsageExamples.Form as Form
|
||||
|
||||
|
||||
all : List (UsageExample State Msg)
|
||||
all =
|
||||
[ ClickableCardWithTooltip.example
|
||||
|> UsageExample.wrapMsg ClickableCardWithTooltipMsg
|
||||
(\msg ->
|
||||
case msg of
|
||||
ClickableCardWithTooltipMsg childMsg ->
|
||||
Just childMsg
|
||||
|
||||
_ ->
|
||||
Nothing
|
||||
)
|
||||
|> UsageExample.wrapState ClickableCardWithTooltipState
|
||||
(\msg ->
|
||||
case msg of
|
||||
ClickableCardWithTooltipState childState ->
|
||||
Just childState
|
||||
|
||||
_ ->
|
||||
Nothing
|
||||
)
|
||||
, Form.example
|
||||
|> UsageExample.wrapMsg FormMsg
|
||||
(\msg ->
|
||||
case msg of
|
||||
FormMsg childMsg ->
|
||||
Just childMsg
|
||||
|
||||
_ ->
|
||||
Nothing
|
||||
)
|
||||
|> UsageExample.wrapState FormState
|
||||
(\msg ->
|
||||
case msg of
|
||||
FormState childState ->
|
||||
Just childState
|
||||
|
||||
_ ->
|
||||
Nothing
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
type State
|
||||
= ClickableCardWithTooltipState ClickableCardWithTooltip.State
|
||||
| FormState Form.State
|
||||
|
||||
|
||||
type Msg
|
||||
= ClickableCardWithTooltipMsg ClickableCardWithTooltip.Msg
|
||||
| FormMsg Form.Msg
|
@ -0,0 +1,99 @@
|
||||
module UsageExamples.ClickableCardWithTooltip exposing (example, State, Msg)
|
||||
|
||||
{-|
|
||||
|
||||
@docs example, State, Msg
|
||||
|
||||
-}
|
||||
|
||||
import Category exposing (Category(..))
|
||||
import Css
|
||||
import Html.Styled exposing (Html)
|
||||
import Html.Styled.Events as Events
|
||||
import Nri.Ui.ClickableText.V3 as ClickableText
|
||||
import Nri.Ui.Container.V2 as Container
|
||||
import Nri.Ui.Text.V6 as Text
|
||||
import Nri.Ui.Tooltip.V3 as Tooltip
|
||||
import UsageExample exposing (UsageExample)
|
||||
|
||||
|
||||
example : UsageExample State Msg
|
||||
example =
|
||||
{ name = "Clickable Card with Tooltip"
|
||||
, categories = [ Messaging ]
|
||||
, state = init
|
||||
, update = update
|
||||
, subscriptions = \_ -> Sub.none
|
||||
, about = []
|
||||
, view = view
|
||||
}
|
||||
|
||||
|
||||
type alias State =
|
||||
{ openTooltip : Maybe Tooltip
|
||||
, parentClicks : Int
|
||||
}
|
||||
|
||||
|
||||
init : State
|
||||
init =
|
||||
{ openTooltip = Nothing
|
||||
, parentClicks = 0
|
||||
}
|
||||
|
||||
|
||||
type alias Tooltip =
|
||||
()
|
||||
|
||||
|
||||
type Msg
|
||||
= ToggleTooltip Tooltip Bool
|
||||
| ParentClick
|
||||
|
||||
|
||||
update : Msg -> State -> ( State, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
ToggleTooltip tooltip True ->
|
||||
( { model | openTooltip = Just tooltip }, Cmd.none )
|
||||
|
||||
ToggleTooltip _ False ->
|
||||
( { model | openTooltip = Nothing }, Cmd.none )
|
||||
|
||||
ParentClick ->
|
||||
( { model | parentClicks = model.parentClicks + 1 }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
|
||||
view : State -> List (Html Msg)
|
||||
view model =
|
||||
[ Container.view
|
||||
[ Container.buttony
|
||||
, Container.html
|
||||
[ Text.smallBody
|
||||
[ Text.html
|
||||
[ ClickableText.button "Click me" [ ClickableText.appearsInline ]
|
||||
, viewTooltip model
|
||||
]
|
||||
]
|
||||
, Text.smallBody
|
||||
[ Text.plaintext "…or click anywhere in the Container!"
|
||||
, Text.id "container-element"
|
||||
]
|
||||
]
|
||||
, Container.custom [ Events.onClick ParentClick ]
|
||||
, Container.css [ Css.maxWidth (Css.px 500) ]
|
||||
]
|
||||
, Text.mediumBody [ Text.plaintext ("Parent Clicks: " ++ String.fromInt model.parentClicks) ]
|
||||
]
|
||||
|
||||
|
||||
viewTooltip : State -> Html Msg
|
||||
viewTooltip model =
|
||||
Tooltip.viewToggleTip { label = "Tooltip trigger", lastId = Nothing }
|
||||
[ Tooltip.plaintext "Notice that even though this tooltip is in a clickable card, you can still interact with me!"
|
||||
, Tooltip.onToggle (ToggleTooltip ())
|
||||
, Tooltip.open (model.openTooltip == Just ())
|
||||
, Tooltip.onRight
|
||||
]
|
106
component-catalog/src/UsageExamples/Form.elm
Normal file
106
component-catalog/src/UsageExamples/Form.elm
Normal file
@ -0,0 +1,106 @@
|
||||
module UsageExamples.Form exposing (example, State, Msg)
|
||||
|
||||
{-|
|
||||
|
||||
@docs example, State, Msg
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled exposing (..)
|
||||
import Category exposing (Category(..))
|
||||
import Css
|
||||
import Dict exposing (Dict)
|
||||
import Html.Styled exposing (Html)
|
||||
import Html.Styled.Attributes exposing (css)
|
||||
import Nri.Ui.Select.V9 as Select
|
||||
import Nri.Ui.TextInput.V7 as TextInput
|
||||
import UsageExample exposing (UsageExample)
|
||||
|
||||
|
||||
example : UsageExample State Msg
|
||||
example =
|
||||
{ name = "Form"
|
||||
, categories = [ Inputs ]
|
||||
, state = init
|
||||
, update = update
|
||||
, subscriptions = \_ -> Sub.none
|
||||
, about = []
|
||||
, view = view
|
||||
}
|
||||
|
||||
|
||||
type alias State =
|
||||
{ title : Maybe String
|
||||
, firstName : String
|
||||
, lastName : String
|
||||
, errors : Dict String String
|
||||
}
|
||||
|
||||
|
||||
init : State
|
||||
init =
|
||||
{ title = Nothing
|
||||
, firstName = ""
|
||||
, lastName = ""
|
||||
, errors = Dict.empty
|
||||
}
|
||||
|
||||
|
||||
type Msg
|
||||
= SelectTitle String
|
||||
| SetFirstName String
|
||||
| SetLastName String
|
||||
|
||||
|
||||
update : Msg -> State -> ( State, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
SelectTitle title ->
|
||||
( { model | title = Just title }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
SetFirstName name ->
|
||||
( { model | firstName = name }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
SetLastName name ->
|
||||
( { model | lastName = name }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
|
||||
view : State -> List (Html Msg)
|
||||
view model =
|
||||
[ form []
|
||||
[ div
|
||||
[ css
|
||||
[ Css.displayFlex
|
||||
, Css.property "gap" "10px"
|
||||
]
|
||||
]
|
||||
[ Select.view "Title"
|
||||
[ Select.value model.title
|
||||
, Select.choices identity
|
||||
(List.map
|
||||
(\title -> { label = title, value = title })
|
||||
[ "Ms.", "Mrs.", "Mr.", "Mx.", "Dr." ]
|
||||
)
|
||||
, Select.id "user_title"
|
||||
, Select.errorMessage (Dict.get "title" model.errors)
|
||||
]
|
||||
|> map SelectTitle
|
||||
, TextInput.view "First name"
|
||||
[ TextInput.value model.firstName
|
||||
, TextInput.givenName SetFirstName
|
||||
, TextInput.errorMessage (Dict.get "first_name" model.errors)
|
||||
]
|
||||
, TextInput.view "Last name"
|
||||
[ TextInput.value model.lastName
|
||||
, TextInput.familyName SetLastName
|
||||
, TextInput.errorMessage (Dict.get "last_name" model.errors)
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
@ -1,6 +1,6 @@
|
||||
module HighlighterExampleSpec exposing (suite)
|
||||
|
||||
import Examples.Highlighter exposing (Msg, State, example)
|
||||
import Examples.Highlighter exposing (example)
|
||||
import MouseHelpers
|
||||
import ProgramTest exposing (..)
|
||||
import PseudoElements exposing (hasAfter, hasBefore)
|
||||
@ -10,9 +10,9 @@ import Test.Html.Selector exposing (..)
|
||||
import TestApp exposing (app)
|
||||
|
||||
|
||||
route : Route State Msg
|
||||
route : Route
|
||||
route =
|
||||
Routes.Doodad example
|
||||
Routes.exampleRoute example
|
||||
|
||||
|
||||
suite : Test
|
||||
|
@ -2,7 +2,7 @@ module SwitchExampleSpec exposing (suite)
|
||||
|
||||
import Accessibility.Aria as Aria
|
||||
import Accessibility.Role as Role
|
||||
import Examples.Switch exposing (Msg, State, example)
|
||||
import Examples.Switch exposing (example)
|
||||
import ProgramTest exposing (..)
|
||||
import Routes exposing (Route)
|
||||
import Test exposing (..)
|
||||
@ -12,9 +12,9 @@ import Test.Html.Selector exposing (..)
|
||||
import TestApp exposing (app)
|
||||
|
||||
|
||||
route : Route State Msg
|
||||
route : Route
|
||||
route =
|
||||
Routes.Doodad example
|
||||
Routes.exampleRoute example
|
||||
|
||||
|
||||
suite : Test
|
||||
|
@ -46,6 +46,12 @@ describe("UI tests", function () {
|
||||
server.close();
|
||||
});
|
||||
|
||||
const hasText = async (xPathSelector = "//html", text) => {
|
||||
let [node] = await page.$x(xPathSelector);
|
||||
let innerText = await page.evaluate((el) => el.innerText, node);
|
||||
assert.equal(innerText, text);
|
||||
};
|
||||
|
||||
const handlePageErrors = function (page) {
|
||||
page.on("pageerror", (err) => {
|
||||
console.log("Error from page:", err.toString());
|
||||
@ -68,7 +74,7 @@ describe("UI tests", function () {
|
||||
}
|
||||
};
|
||||
|
||||
const goTo = async (name, location) => {
|
||||
const goToExample = async (name, location) => {
|
||||
await page.goto(location, { waitUntil: "load" });
|
||||
await page.waitForXPath(
|
||||
`//h1[contains(., 'Nri.Ui.${name}') and @aria-current='page']`,
|
||||
@ -77,7 +83,7 @@ describe("UI tests", function () {
|
||||
};
|
||||
|
||||
const defaultProcessing = async (name, location) => {
|
||||
await goTo(name, location);
|
||||
await goToExample(name, location);
|
||||
await percySnapshot(page, name);
|
||||
|
||||
const results = await new AxePuppeteer(page)
|
||||
@ -86,6 +92,20 @@ describe("UI tests", function () {
|
||||
handleAxeResults(name, results);
|
||||
};
|
||||
|
||||
const defaultUsageExampleProcessing = async (testName, name, location) => {
|
||||
await page.goto(location, { waitUntil: "load" });
|
||||
await page.waitForXPath(
|
||||
`//h1[contains(., '${name}') and @aria-current='page']`,
|
||||
200
|
||||
);
|
||||
await percySnapshot(page, name);
|
||||
|
||||
const results = await new AxePuppeteer(page)
|
||||
.disableRules(skippedRules[testName] || [])
|
||||
.analyze();
|
||||
handleAxeResults(name, results);
|
||||
};
|
||||
|
||||
const forAllOptions = async (labelName, callback) => {
|
||||
await page.waitForXPath(
|
||||
`//label[contains(., '${labelName}')]//select`,
|
||||
@ -107,7 +127,7 @@ describe("UI tests", function () {
|
||||
};
|
||||
|
||||
const messageProcessing = async (name, location) => {
|
||||
await goTo(name, location);
|
||||
await goToExample(name, location);
|
||||
await percySnapshot(page, name);
|
||||
|
||||
var axe = await new AxePuppeteer(page)
|
||||
@ -128,7 +148,7 @@ describe("UI tests", function () {
|
||||
};
|
||||
|
||||
const modalProcessing = async (name, location) => {
|
||||
await goTo(name, location);
|
||||
await goToExample(name, location);
|
||||
|
||||
await page.click("#launch-modal");
|
||||
await page.waitForSelector('[role="dialog"]');
|
||||
@ -142,7 +162,7 @@ describe("UI tests", function () {
|
||||
};
|
||||
|
||||
const pageProcessing = async (name, location) => {
|
||||
await goTo(name, location);
|
||||
await goToExample(name, location);
|
||||
|
||||
var axe = await new AxePuppeteer(page)
|
||||
.disableRules(skippedRules[name] || [])
|
||||
@ -170,59 +190,49 @@ describe("UI tests", function () {
|
||||
handleAxeResults(name, results);
|
||||
};
|
||||
|
||||
const tooltipProcessing = async (name, location) => {
|
||||
await defaultProcessing(name, location);
|
||||
await page.waitForSelector("#parent-button-clicks");
|
||||
|
||||
const buttonClick = async () => {
|
||||
const button = await page.$("#parent-button");
|
||||
await page.evaluate((el) => el.click(), button);
|
||||
const clickableCardWithTooltipProcessing = async (
|
||||
testName,
|
||||
name,
|
||||
location
|
||||
) => {
|
||||
const hasParentClicks = async (count) => {
|
||||
await page.waitForTimeout(100);
|
||||
await hasText(
|
||||
"//p[contains(., 'Parent Clicks')]",
|
||||
`Parent Clicks: ${count}`
|
||||
);
|
||||
};
|
||||
|
||||
const getCounterText = async () => {
|
||||
const counter = await page.$("#parent-button-clicks");
|
||||
const text = await page.evaluate((el) => el.innerText, counter);
|
||||
return text;
|
||||
};
|
||||
await defaultUsageExampleProcessing(testName, name, location);
|
||||
|
||||
const tooltipTriggerClick = async () => {
|
||||
const button = await page.$("#tooltip__disclosure-trigger");
|
||||
await page.evaluate((el) => el.click(), button);
|
||||
await page.waitForTimeout(100);
|
||||
};
|
||||
await hasParentClicks(0);
|
||||
await page.waitForSelector("[data-tooltip-visible=false]");
|
||||
|
||||
const isTooltipVisible = async () => {
|
||||
let res = await page.evaluate(() => {
|
||||
return (
|
||||
getComputedStyle(
|
||||
document.getElementById("tooltip__disclosure").parentElement
|
||||
).getPropertyValue("display") != "none"
|
||||
);
|
||||
});
|
||||
// Opening and closing the tooltip doesn't trigger the container effects
|
||||
await page.hover('[aria-label="Tooltip trigger"]');
|
||||
await page.waitForSelector("[data-tooltip-visible=true]");
|
||||
|
||||
return res;
|
||||
};
|
||||
await page.click('[aria-label="Tooltip trigger"]');
|
||||
await page.waitForSelector("[data-tooltip-visible=false]");
|
||||
|
||||
assert.equal(await getCounterText(), "Parent Clicks: 0");
|
||||
assert.equal(await isTooltipVisible(), false);
|
||||
await tooltipTriggerClick();
|
||||
assert.equal(await isTooltipVisible(), true);
|
||||
await tooltipTriggerClick();
|
||||
assert.equal(await isTooltipVisible(), false);
|
||||
assert.equal(await getCounterText(), "Parent Clicks: 0");
|
||||
await buttonClick();
|
||||
assert.equal(await getCounterText(), "Parent Clicks: 1");
|
||||
await buttonClick();
|
||||
assert.equal(await getCounterText(), "Parent Clicks: 2");
|
||||
await hasParentClicks(0);
|
||||
|
||||
// Clicking the button does trigger container effects
|
||||
const [button] = await page.$x("//button[contains(., 'Click me')]");
|
||||
await button.click();
|
||||
|
||||
await page.waitForSelector("[data-tooltip-visible=false]");
|
||||
await hasParentClicks(1);
|
||||
|
||||
// Clicking the container does trigger container effects
|
||||
await page.click("#container-element");
|
||||
await hasParentClicks(2);
|
||||
};
|
||||
|
||||
const skippedRules = {
|
||||
// Loading's color contrast check seems to change behavior depending on whether Percy snapshots are taken or not
|
||||
Loading: ["color-contrast"],
|
||||
RadioButton: ["duplicate-id"],
|
||||
// We need nested-interactive to test the tooltip behavior
|
||||
Tooltip: ["nested-interactive"],
|
||||
};
|
||||
|
||||
const specialProcessing = {
|
||||
@ -233,33 +243,38 @@ describe("UI tests", function () {
|
||||
UiIcon: iconProcessing,
|
||||
Logo: iconProcessing,
|
||||
Pennant: iconProcessing,
|
||||
Tooltip: tooltipProcessing,
|
||||
};
|
||||
|
||||
const specialUsageProcessing = {
|
||||
ClickableCardwithTooltip: clickableCardWithTooltipProcessing,
|
||||
};
|
||||
|
||||
it("All", async function () {
|
||||
page = await browser.newPage();
|
||||
if (process.env.ONLYDOODAD == "default") {
|
||||
page = await browser.newPage();
|
||||
|
||||
await page.emulateMediaFeatures([
|
||||
{ name: "prefers-reduced-motion", value: "reduce" },
|
||||
]);
|
||||
await page.emulateMediaFeatures([
|
||||
{ name: "prefers-reduced-motion", value: "reduce" },
|
||||
]);
|
||||
|
||||
handlePageErrors(page);
|
||||
await page.goto(`http://localhost:${PORT}`, { waitUntil: "load" });
|
||||
await page.$("#maincontent");
|
||||
await percySnapshot(page, this.test.fullTitle());
|
||||
handlePageErrors(page);
|
||||
await page.goto(`http://localhost:${PORT}`, { waitUntil: "load" });
|
||||
await page.$("#maincontent");
|
||||
await percySnapshot(page, this.test.fullTitle());
|
||||
|
||||
const results = await new AxePuppeteer(page)
|
||||
.disableRules([
|
||||
"aria-hidden-focus",
|
||||
"color-contrast",
|
||||
"duplicate-id-aria",
|
||||
"duplicate-id",
|
||||
])
|
||||
.analyze();
|
||||
const results = await new AxePuppeteer(page)
|
||||
.disableRules([
|
||||
"aria-hidden-focus",
|
||||
"color-contrast",
|
||||
"duplicate-id-aria",
|
||||
"duplicate-id",
|
||||
])
|
||||
.analyze();
|
||||
|
||||
page.close();
|
||||
page.close();
|
||||
|
||||
handleAxeResults("index view", results);
|
||||
handleAxeResults("index view", results);
|
||||
}
|
||||
});
|
||||
|
||||
it("Doodads", async function () {
|
||||
@ -295,4 +310,40 @@ describe("UI tests", function () {
|
||||
|
||||
page.close();
|
||||
});
|
||||
|
||||
it("Usage examples", async function () {
|
||||
page = await browser.newPage();
|
||||
|
||||
await page.emulateMediaFeatures([
|
||||
{ name: "prefers-reduced-motion", value: "reduce" },
|
||||
]);
|
||||
|
||||
handlePageErrors(page);
|
||||
await page.goto(`http://localhost:${PORT}`);
|
||||
|
||||
await page.$("#maincontent");
|
||||
let links = await page.evaluate(() => {
|
||||
let nodes = Array.from(
|
||||
document.querySelectorAll("[data-nri-description='usage-example-link']")
|
||||
);
|
||||
return nodes.map((node) => [node.text, node.href]);
|
||||
});
|
||||
|
||||
await links.reduce((acc, [name, location]) => {
|
||||
return acc.then(() => {
|
||||
let testName = name.replaceAll(" ", "");
|
||||
if (
|
||||
process.env.ONLYDOODAD == "default" ||
|
||||
process.env.ONLYDOODAD == testName
|
||||
) {
|
||||
console.log(`Testing Usage Example ${testName}`);
|
||||
let handler =
|
||||
specialUsageProcessing[testName] || defaultUsageExampleProcessing;
|
||||
return handler(testName, name, location);
|
||||
}
|
||||
});
|
||||
}, Promise.resolve());
|
||||
|
||||
page.close();
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user