mirror of
https://github.com/NoRedInk/noredink-ui.git
synced 2024-11-05 00:29:17 +03:00
Keep Block.V3 and QuestionBox.V2 around for a while longer
This commit is contained in:
parent
f455f980b7
commit
573b8ecb4e
@ -1 +1,3 @@
|
||||
Nri.Ui.Block.V3,upgrade to V4
|
||||
Nri.Ui.QuestionBox.V2,upgrade to V3
|
||||
Nri.Ui.Tabs.V6,upgrade to V7
|
||||
|
|
2
elm.json
2
elm.json
@ -11,6 +11,7 @@
|
||||
"Nri.Ui.AnimatedIcon.V1",
|
||||
"Nri.Ui.AssignmentIcon.V2",
|
||||
"Nri.Ui.Balloon.V2",
|
||||
"Nri.Ui.Block.V3",
|
||||
"Nri.Ui.Block.V4",
|
||||
"Nri.Ui.BreadCrumbs.V2",
|
||||
"Nri.Ui.Button.V10",
|
||||
@ -54,6 +55,7 @@
|
||||
"Nri.Ui.Panel.V1",
|
||||
"Nri.Ui.Pennant.V2",
|
||||
"Nri.Ui.PremiumCheckbox.V8",
|
||||
"Nri.Ui.QuestionBox.V2",
|
||||
"Nri.Ui.QuestionBox.V3",
|
||||
"Nri.Ui.RadioButton.V4",
|
||||
"Nri.Ui.RingGauge.V1",
|
||||
|
615
src/Nri/Ui/Block/V3.elm
Normal file
615
src/Nri/Ui/Block/V3.elm
Normal file
@ -0,0 +1,615 @@
|
||||
module Nri.Ui.Block.V3 exposing
|
||||
( view, Attribute
|
||||
, plaintext
|
||||
, Content, content
|
||||
, phrase, wordWithQuestionBox
|
||||
, space
|
||||
, blank, blankWithQuestionBox
|
||||
, emphasize
|
||||
, label
|
||||
, labelId, labelContentId
|
||||
, LabelPosition, getLabelPositions, labelPosition
|
||||
, yellow, cyan, magenta, green, blue, purple, brown
|
||||
, withQuestionBox
|
||||
)
|
||||
|
||||
{-|
|
||||
|
||||
@docs view, Attribute
|
||||
|
||||
|
||||
## Content
|
||||
|
||||
@docs plaintext
|
||||
@docs Content, content
|
||||
@docs phrase, wordWithQuestionBox
|
||||
@docs space
|
||||
@docs blank, blankWithQuestionBox
|
||||
|
||||
|
||||
## Content customization
|
||||
|
||||
@docs emphasize
|
||||
|
||||
|
||||
## Labels & positioning
|
||||
|
||||
@docs label
|
||||
|
||||
You will need these helpers if you want to prevent label overlaps. (Which is to say -- anytime you have labels!)
|
||||
|
||||
@docs labelId, labelContentId
|
||||
@docs LabelPosition, getLabelPositions, labelPosition
|
||||
|
||||
|
||||
### Visual customization
|
||||
|
||||
@docs yellow, cyan, magenta, green, blue, purple, brown
|
||||
|
||||
|
||||
### Add a question box
|
||||
|
||||
@docs withQuestionBox
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled exposing (..)
|
||||
import Browser.Dom as Dom
|
||||
import Css exposing (Color)
|
||||
import Dict exposing (Dict)
|
||||
import Html.Styled.Attributes exposing (css)
|
||||
import List.Extra
|
||||
import Nri.Ui.Colors.V1 as Colors
|
||||
import Nri.Ui.Html.Attributes.V2 exposing (nriDescription)
|
||||
import Nri.Ui.Mark.V2 as Mark exposing (Mark)
|
||||
import Nri.Ui.MediaQuery.V1 as MediaQuery
|
||||
import Nri.Ui.QuestionBox.V2 as QuestionBox
|
||||
import Position exposing (xOffsetPx)
|
||||
|
||||
|
||||
{-|
|
||||
|
||||
Block.view [ Block.plaintext "Hello, world!" ]
|
||||
|
||||
-}
|
||||
view : List (Attribute msg) -> Html msg
|
||||
view attributes =
|
||||
attributes
|
||||
|> List.foldl (\(Attribute attribute) b -> attribute b) defaultConfig
|
||||
|> render
|
||||
|
||||
|
||||
|
||||
-- Attributes
|
||||
|
||||
|
||||
{-| Provide the main content of the block as a plain-text string. You can also use `content` for more complex cases, including a blank appearing within an emphasis.
|
||||
-}
|
||||
plaintext : String -> Attribute msg
|
||||
plaintext content_ =
|
||||
Attribute <| \config -> { config | content = parseString content_ }
|
||||
|
||||
|
||||
{-| Use `content` for more complex block views, for instance when a blank appears within an emphasis block. Prefer to use `plaintext` when possible for better readability.
|
||||
|
||||
Block.view
|
||||
[ Block.emphasize
|
||||
, Block.content (Block.phrase "Hello, " ++ Block.blank :: Block.phrase "!" ) ]
|
||||
]
|
||||
|
||||
-}
|
||||
content : List (Content msg) -> Attribute msg
|
||||
content content_ =
|
||||
Attribute <| \config -> { config | content = content_ }
|
||||
|
||||
|
||||
{-| Mark content as emphasized.
|
||||
-}
|
||||
emphasize : Attribute msg
|
||||
emphasize =
|
||||
Attribute <| \config -> { config | emphasize = True }
|
||||
|
||||
|
||||
{-| -}
|
||||
label : String -> Attribute msg
|
||||
label label_ =
|
||||
Attribute <| \config -> { config | label = Just label_, emphasize = True }
|
||||
|
||||
|
||||
{-| Use `getLabelPositions` to construct this value.
|
||||
-}
|
||||
type alias LabelPosition =
|
||||
{ totalHeight : Float
|
||||
, arrowHeight : Float
|
||||
, zIndex : Int
|
||||
, xOffset : Float
|
||||
}
|
||||
|
||||
|
||||
{-| Use `getLabelPositions` to calculate what these values should be.
|
||||
-}
|
||||
labelPosition : Maybe LabelPosition -> Attribute msg
|
||||
labelPosition offset =
|
||||
Attribute <| \config -> { config | labelPosition = offset }
|
||||
|
||||
|
||||
{-| -}
|
||||
labelContentId : String -> String
|
||||
labelContentId labelId_ =
|
||||
labelId_ ++ "-label-content"
|
||||
|
||||
|
||||
{-| Determine where to position labels in order to dynamically avoid overlapping content.
|
||||
|
||||
- First, we add ids to block with labels with `Block.labelId`.
|
||||
- Say we added `Block.labelId "example-id"`, then we will use `Browser.Dom.getElement "example-id"` and `Browser.Dom.getElement (Block.labelContentId "example-id")` to construct a record in the shape { label : Dom.Element, labelContent : Dom.Element }. We store this record in a dictionary keyed by ids (e.g., "example-id") with measurements for all other labels.
|
||||
|
||||
`getLabelPositions` will return a dictionary of values (keyed by label ids) whose values can be passed directly to `labelPosition` for positioning.
|
||||
|
||||
-}
|
||||
getLabelPositions :
|
||||
Dict String { label : Dom.Element, labelContent : Dom.Element }
|
||||
-> Dict String LabelPosition
|
||||
getLabelPositions labelMeasurementsById =
|
||||
let
|
||||
startingArrowHeight =
|
||||
8
|
||||
in
|
||||
Dict.toList labelMeasurementsById
|
||||
|> splitByOverlaps
|
||||
|> List.concatMap
|
||||
(\row ->
|
||||
let
|
||||
maxRowIndex =
|
||||
List.length row - 1
|
||||
in
|
||||
row
|
||||
-- Put the widest elements higher visually to avoid overlaps
|
||||
|> List.sortBy (Tuple.second >> .labelContent >> .element >> .width)
|
||||
|> List.foldl
|
||||
(\( idString, e ) ( index, height, acc ) ->
|
||||
( index + 1
|
||||
, height + e.labelContent.element.height + 4
|
||||
, ( idString
|
||||
, { totalHeight = height + e.labelContent.element.height
|
||||
, arrowHeight = height
|
||||
, zIndex = maxRowIndex - index
|
||||
, xOffset = xOffsetPx e.label
|
||||
}
|
||||
)
|
||||
:: acc
|
||||
)
|
||||
)
|
||||
( 0, startingArrowHeight, [] )
|
||||
|> (\( _, _, v ) -> v)
|
||||
)
|
||||
|> Dict.fromList
|
||||
|
||||
|
||||
{-| Group the elements whose bottom edges are at the same height. This ensures that we only offset labels against other labels in the same visual line of content.
|
||||
|
||||
Then, for elements in the same row, group elements with horizontal overlaps .
|
||||
|
||||
-}
|
||||
splitByOverlaps : List ( id, { a | label : Dom.Element } ) -> List (List ( id, { a | label : Dom.Element } ))
|
||||
splitByOverlaps =
|
||||
-- consider the elements from top to bottom
|
||||
groupWithSort (\( _, a ) -> a.label.element.y + a.label.element.height)
|
||||
(\( _, a ) ( _, b ) ->
|
||||
(a.label.element.y + a.label.element.height) == (b.label.element.y + b.label.element.height)
|
||||
)
|
||||
>> List.concatMap
|
||||
-- consider the elements from left to right
|
||||
(groupWithSort (\( _, a ) -> a.label.element.x)
|
||||
(\( _, a ) ( _, b ) ->
|
||||
(a.label.element.x + xOffsetPx a.label + a.label.element.width) >= (b.label.element.x + xOffsetPx b.label)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
groupWithSort : (a -> comparable) -> (a -> a -> Bool) -> List a -> List (List a)
|
||||
groupWithSort sortBy groupBy =
|
||||
List.sortBy sortBy
|
||||
>> List.Extra.groupWhile groupBy
|
||||
>> List.map (\( first, rem ) -> first :: rem)
|
||||
|
||||
|
||||
|
||||
-- Content
|
||||
|
||||
|
||||
{-| -}
|
||||
type Content msg
|
||||
= Word (List (QuestionBox.Attribute msg)) (Maybe Dom.Element) String
|
||||
| Blank (List (QuestionBox.Attribute msg)) (Maybe Dom.Element)
|
||||
| FullHeightBlank
|
||||
|
||||
|
||||
parseString : String -> List (Content msg)
|
||||
parseString =
|
||||
String.split " "
|
||||
>> List.intersperse " "
|
||||
>> List.filter (\str -> str /= "")
|
||||
>> List.map (Word [] Nothing)
|
||||
|
||||
|
||||
renderContent :
|
||||
{ config | questionBoxElement : Maybe Dom.Element }
|
||||
-> Content msg
|
||||
-> List Css.Style
|
||||
-> Html msg
|
||||
renderContent config content_ markStyles =
|
||||
let
|
||||
marginBottom override =
|
||||
case ( config.questionBoxElement, override ) of
|
||||
( Nothing, Just by ) ->
|
||||
Css.important (Css.marginBottom (Css.px (by.element.height + 8)))
|
||||
|
||||
( Just by, _ ) ->
|
||||
Css.important (Css.marginBottom (Css.px (by.element.height + 8)))
|
||||
|
||||
_ ->
|
||||
Css.batch []
|
||||
|
||||
viewContainer marginOverride =
|
||||
blockSegmentContainer (marginBottom marginOverride :: markStyles)
|
||||
in
|
||||
case content_ of
|
||||
Word [] _ str ->
|
||||
viewContainer Nothing [ text str ]
|
||||
|
||||
Word questionBoxAttributes element str ->
|
||||
viewContainer element
|
||||
[ text str
|
||||
, QuestionBox.view
|
||||
(QuestionBox.pointingTo element
|
||||
:: questionBoxAttributes
|
||||
)
|
||||
]
|
||||
|
||||
Blank [] _ ->
|
||||
viewContainer Nothing
|
||||
[ viewBlank [ Css.lineHeight (Css.num 1) ] ]
|
||||
|
||||
Blank questionBoxAttributes element ->
|
||||
viewContainer element
|
||||
[ viewBlank [ Css.lineHeight (Css.num 1) ]
|
||||
, QuestionBox.view
|
||||
(QuestionBox.pointingTo element
|
||||
:: questionBoxAttributes
|
||||
)
|
||||
]
|
||||
|
||||
FullHeightBlank ->
|
||||
viewContainer Nothing
|
||||
[ viewBlank
|
||||
[ Css.paddingTop topBottomSpace
|
||||
, Css.paddingBottom topBottomSpace
|
||||
, Css.lineHeight Css.initial
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
blockSegmentContainer : List Css.Style -> List (Html msg) -> Html msg
|
||||
blockSegmentContainer styles =
|
||||
span
|
||||
[ css
|
||||
(Css.whiteSpace Css.preWrap
|
||||
:: Css.display Css.inlineBlock
|
||||
:: Css.position Css.relative
|
||||
:: styles
|
||||
)
|
||||
, nriDescription "block-segment-container"
|
||||
]
|
||||
|
||||
|
||||
{-| -}
|
||||
phrase : String -> List (Content msg)
|
||||
phrase =
|
||||
parseString
|
||||
|
||||
|
||||
{-| -}
|
||||
space : Content msg
|
||||
space =
|
||||
Word [] Nothing " "
|
||||
|
||||
|
||||
{-| -}
|
||||
wordWithQuestionBox : String -> List (QuestionBox.Attribute msg) -> Maybe Dom.Element -> Content msg
|
||||
wordWithQuestionBox str attributes element =
|
||||
Word attributes element str
|
||||
|
||||
|
||||
{-| You will only need to use this helper if you're also using `content` to construct a more complex Block. For a less complex blank Block, don't include content or plaintext in the list of attributes.
|
||||
-}
|
||||
blank : Content msg
|
||||
blank =
|
||||
Blank [] Nothing
|
||||
|
||||
|
||||
{-| You will only need to use this helper if you're also using `content` to construct a more complex Block. For a less complex blank Block, don't include content or plaintext in the list of attributes.
|
||||
-}
|
||||
blankWithQuestionBox : List (QuestionBox.Attribute msg) -> Maybe Dom.Element -> Content msg
|
||||
blankWithQuestionBox attributes element =
|
||||
Blank attributes element
|
||||
|
||||
|
||||
|
||||
-- Color themes
|
||||
|
||||
|
||||
{-| -}
|
||||
type Theme
|
||||
= Yellow
|
||||
| Cyan
|
||||
| Magenta
|
||||
| Green
|
||||
| Blue
|
||||
| Purple
|
||||
| Brown
|
||||
|
||||
|
||||
themeToPalette : Theme -> Palette
|
||||
themeToPalette theme =
|
||||
case theme of
|
||||
Yellow ->
|
||||
{ backgroundColor = Colors.highlightYellow
|
||||
, borderColor = Colors.highlightYellowDark
|
||||
}
|
||||
|
||||
Cyan ->
|
||||
{ backgroundColor = Colors.highlightCyan
|
||||
, borderColor = Colors.highlightCyanDark
|
||||
}
|
||||
|
||||
Magenta ->
|
||||
{ backgroundColor = Colors.highlightMagenta
|
||||
, borderColor = Colors.highlightMagentaDark
|
||||
}
|
||||
|
||||
Green ->
|
||||
{ backgroundColor = Colors.highlightGreen
|
||||
, borderColor = Colors.highlightGreenDark
|
||||
}
|
||||
|
||||
Blue ->
|
||||
{ backgroundColor = Colors.highlightBlue
|
||||
, borderColor = Colors.highlightBlueDark
|
||||
}
|
||||
|
||||
Purple ->
|
||||
{ backgroundColor = Colors.highlightPurple
|
||||
, borderColor = Colors.highlightPurpleDark
|
||||
}
|
||||
|
||||
Brown ->
|
||||
{ backgroundColor = Colors.highlightBrown
|
||||
, borderColor = Colors.highlightBrownDark
|
||||
}
|
||||
|
||||
|
||||
type alias Palette =
|
||||
{ backgroundColor : Color, borderColor : Color }
|
||||
|
||||
|
||||
toMark :
|
||||
{ config
|
||||
| emphasize : Bool
|
||||
, label : Maybe String
|
||||
, content : List (Content msg)
|
||||
}
|
||||
-> Palette
|
||||
-> Maybe Mark
|
||||
toMark config { backgroundColor, borderColor } =
|
||||
case ( config.label, config.content, config.emphasize ) of
|
||||
( Just l, FullHeightBlank :: [], _ ) ->
|
||||
Just
|
||||
{ name = Just l
|
||||
, startStyles = []
|
||||
, styles = []
|
||||
, endStyles = []
|
||||
}
|
||||
|
||||
( Just l, _, False ) ->
|
||||
Just
|
||||
{ name = Just l
|
||||
, startStyles = []
|
||||
, styles = []
|
||||
, endStyles = []
|
||||
}
|
||||
|
||||
( _, _, True ) ->
|
||||
let
|
||||
borderWidth =
|
||||
Css.px 1
|
||||
|
||||
borderStyle =
|
||||
Css.dashed
|
||||
in
|
||||
Just
|
||||
{ name = config.label
|
||||
, startStyles =
|
||||
[ Css.borderLeft3 borderWidth borderStyle borderColor
|
||||
, Css.paddingLeft (Css.px 2)
|
||||
]
|
||||
, styles =
|
||||
[ Css.paddingTop topBottomSpace
|
||||
, Css.paddingBottom topBottomSpace
|
||||
, Css.backgroundColor backgroundColor
|
||||
, Css.borderTop3 borderWidth borderStyle borderColor
|
||||
, Css.borderBottom3 borderWidth borderStyle borderColor
|
||||
, MediaQuery.highContrastMode
|
||||
[ Css.property "background-color" "Mark"
|
||||
, Css.property "color" "MarkText"
|
||||
, Css.property "forced-color-adjust" "none"
|
||||
]
|
||||
]
|
||||
, endStyles =
|
||||
[ Css.borderRight3 borderWidth borderStyle borderColor
|
||||
, Css.paddingRight (Css.px 2)
|
||||
]
|
||||
}
|
||||
|
||||
( Nothing, _, False ) ->
|
||||
Nothing
|
||||
|
||||
|
||||
topBottomSpace : Css.Px
|
||||
topBottomSpace =
|
||||
Css.px 4
|
||||
|
||||
|
||||
{-| -}
|
||||
yellow : Attribute msg
|
||||
yellow =
|
||||
Attribute (\config -> { config | theme = Yellow })
|
||||
|
||||
|
||||
{-| -}
|
||||
cyan : Attribute msg
|
||||
cyan =
|
||||
Attribute (\config -> { config | theme = Cyan })
|
||||
|
||||
|
||||
{-| -}
|
||||
magenta : Attribute msg
|
||||
magenta =
|
||||
Attribute (\config -> { config | theme = Magenta })
|
||||
|
||||
|
||||
{-| -}
|
||||
green : Attribute msg
|
||||
green =
|
||||
Attribute (\config -> { config | theme = Green })
|
||||
|
||||
|
||||
{-| -}
|
||||
blue : Attribute msg
|
||||
blue =
|
||||
Attribute (\config -> { config | theme = Blue })
|
||||
|
||||
|
||||
{-| -}
|
||||
purple : Attribute msg
|
||||
purple =
|
||||
Attribute (\config -> { config | theme = Purple })
|
||||
|
||||
|
||||
{-| -}
|
||||
brown : Attribute msg
|
||||
brown =
|
||||
Attribute (\config -> { config | theme = Brown })
|
||||
|
||||
|
||||
{-| -}
|
||||
labelId : String -> Attribute msg
|
||||
labelId id_ =
|
||||
Attribute (\config -> { config | labelId = Just id_ })
|
||||
|
||||
|
||||
{-| Pass in a list of question box attributes and the sizing (taken using Dom.Browser.getElement) of the question box
|
||||
-}
|
||||
withQuestionBox : List (QuestionBox.Attribute msg) -> Maybe Dom.Element -> Attribute msg
|
||||
withQuestionBox attributes element =
|
||||
Attribute (\config -> { config | questionBox = attributes, questionBoxElement = element })
|
||||
|
||||
|
||||
|
||||
-- Internals
|
||||
|
||||
|
||||
{-| -}
|
||||
type Attribute msg
|
||||
= Attribute (Config msg -> Config msg)
|
||||
|
||||
|
||||
defaultConfig : Config msg
|
||||
defaultConfig =
|
||||
{ content = [ FullHeightBlank ]
|
||||
, label = Nothing
|
||||
, labelId = Nothing
|
||||
, labelPosition = Nothing
|
||||
, theme = Yellow
|
||||
, emphasize = False
|
||||
, questionBoxElement = Nothing
|
||||
, questionBox = []
|
||||
}
|
||||
|
||||
|
||||
type alias Config msg =
|
||||
{ content : List (Content msg)
|
||||
, label : Maybe String
|
||||
, labelId : Maybe String
|
||||
, labelPosition : Maybe LabelPosition
|
||||
, theme : Theme
|
||||
, emphasize : Bool
|
||||
, questionBoxElement : Maybe Dom.Element
|
||||
, questionBox : List (QuestionBox.Attribute msg)
|
||||
}
|
||||
|
||||
|
||||
render : Config msg -> Html msg
|
||||
render config =
|
||||
let
|
||||
palette =
|
||||
themeToPalette config.theme
|
||||
|
||||
maybeMark =
|
||||
toMark config palette
|
||||
in
|
||||
span
|
||||
[ css [ Css.position Css.relative ] ]
|
||||
(Mark.viewWithBalloonTags
|
||||
{ renderSegment = renderContent config
|
||||
, backgroundColor = palette.backgroundColor
|
||||
, maybeMarker = maybeMark
|
||||
, labelPosition = config.labelPosition
|
||||
, labelId = config.labelId
|
||||
, labelContentId = Maybe.map labelContentId config.labelId
|
||||
}
|
||||
config.content
|
||||
++ (case config.questionBox of
|
||||
[] ->
|
||||
[]
|
||||
|
||||
_ ->
|
||||
[ QuestionBox.view
|
||||
(QuestionBox.pointingTo config.questionBoxElement
|
||||
:: config.questionBox
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
viewBlank : List Css.Style -> Html msg
|
||||
viewBlank styles =
|
||||
span
|
||||
[ css
|
||||
[ Css.border3 (Css.px 2) Css.dashed Colors.navy
|
||||
, MediaQuery.highContrastMode
|
||||
[ Css.property "border-color" "CanvasText"
|
||||
, Css.property "background-color" "Canvas"
|
||||
]
|
||||
, Css.backgroundColor Colors.white
|
||||
, Css.minWidth (Css.px 80)
|
||||
, Css.display Css.inlineBlock
|
||||
, Css.borderRadius (Css.px 4)
|
||||
, Css.batch styles
|
||||
]
|
||||
]
|
||||
[ blankString ]
|
||||
|
||||
|
||||
blankString : Html msg
|
||||
blankString =
|
||||
span
|
||||
[ css
|
||||
[ Css.overflowX Css.hidden
|
||||
, Css.width (Css.px 0)
|
||||
, Css.display Css.inlineBlock
|
||||
, Css.verticalAlign Css.bottom
|
||||
]
|
||||
]
|
||||
[ text "blank" ]
|
336
src/Nri/Ui/QuestionBox/V2.elm
Normal file
336
src/Nri/Ui/QuestionBox/V2.elm
Normal file
@ -0,0 +1,336 @@
|
||||
module Nri.Ui.QuestionBox.V2 exposing
|
||||
( view, Attribute
|
||||
, id, markdown, actions, character
|
||||
, standalone, pointingTo
|
||||
, containerCss
|
||||
, guidanceId
|
||||
)
|
||||
|
||||
{-|
|
||||
|
||||
@docs view, Attribute
|
||||
|
||||
@docs id, markdown, actions, character
|
||||
@docs standalone, pointingTo
|
||||
@docs containerCss
|
||||
|
||||
@docs guidanceId
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled.Key as Key
|
||||
import Browser.Dom exposing (Element)
|
||||
import Css
|
||||
import Css.Global
|
||||
import Html.Styled exposing (..)
|
||||
import Html.Styled.Attributes as Attributes exposing (css)
|
||||
import Nri.Ui.Balloon.V2 as Balloon
|
||||
import Nri.Ui.Button.V10 as Button
|
||||
import Nri.Ui.CharacterIcon.V1 as CharacterIcon
|
||||
import Nri.Ui.Colors.V1 as Colors
|
||||
import Nri.Ui.Html.Attributes.V2 as AttributesExtra exposing (nriDescription)
|
||||
import Nri.Ui.Svg.V1 as Svg exposing (Svg)
|
||||
import Position exposing (xOffsetPx)
|
||||
|
||||
|
||||
{-| -}
|
||||
type Attribute msg
|
||||
= Attribute (Config msg -> Config msg)
|
||||
|
||||
|
||||
type alias Config msg =
|
||||
{ id : Maybe String
|
||||
, markdown : Maybe String
|
||||
, actions : List { label : String, onClick : msg }
|
||||
, type_ : QuestionBoxType msg
|
||||
, character : Maybe { name : String, icon : Svg }
|
||||
, containerCss : List Css.Style
|
||||
}
|
||||
|
||||
|
||||
defaultConfig : Config msg
|
||||
defaultConfig =
|
||||
{ id = Nothing
|
||||
, markdown = Nothing
|
||||
, actions = []
|
||||
, type_ = Standalone
|
||||
, character = Just { name = "Panda", icon = CharacterIcon.panda }
|
||||
, containerCss = [ Css.maxWidth (Css.px 440) ]
|
||||
}
|
||||
|
||||
|
||||
{-| -}
|
||||
id : String -> Attribute msg
|
||||
id id_ =
|
||||
Attribute (\config -> { config | id = Just id_ })
|
||||
|
||||
|
||||
{-| -}
|
||||
markdown : String -> Attribute msg
|
||||
markdown content =
|
||||
Attribute (\config -> { config | markdown = Just content })
|
||||
|
||||
|
||||
{-| -}
|
||||
actions : List { label : String, onClick : msg } -> Attribute msg
|
||||
actions actions_ =
|
||||
Attribute (\config -> { config | actions = actions_ })
|
||||
|
||||
|
||||
{-| -}
|
||||
character : Maybe { name : String, icon : Svg } -> Attribute msg
|
||||
character details =
|
||||
Attribute (\config -> { config | character = details })
|
||||
|
||||
|
||||
{-| -}
|
||||
containerCss : List Css.Style -> Attribute msg
|
||||
containerCss styles =
|
||||
Attribute (\config -> { config | containerCss = config.containerCss ++ styles })
|
||||
|
||||
|
||||
setType : QuestionBoxType msg -> Attribute msg
|
||||
setType type_ =
|
||||
Attribute (\config -> { config | type_ = type_ })
|
||||
|
||||
|
||||
type QuestionBoxType msg
|
||||
= Standalone
|
||||
| PointingTo (Maybe Element)
|
||||
|
||||
|
||||
{-| This is the default type of question box. It doesn't have a programmatic or direct visual relationship to any piece of content.
|
||||
-}
|
||||
standalone : Attribute msg
|
||||
standalone =
|
||||
setType Standalone
|
||||
|
||||
|
||||
{-| This type of `QuestionBox` is absolutely positioned & has an arrow pointing to its relatively positioned ancestor.
|
||||
|
||||
Typically, you would use this type of `QuestionBox` type with a `Block` by way of `Block.withHtml`.
|
||||
|
||||
You will need to pass a measurement, taken using Dom.Browser, in order for the question box to be positioned correctly horizontally so that it doesn't get cut off by the viewport.
|
||||
|
||||
-}
|
||||
pointingTo : Maybe Element -> Attribute msg
|
||||
pointingTo element =
|
||||
setType (PointingTo element)
|
||||
|
||||
|
||||
{-| This helper is how we create an id for the guidance speech bubble element based on the `QuestionBox.id` value. Use this helper to manage focus.
|
||||
|
||||
When showing multiple questions in a sequence based on the user's answer, we want to move the user's focus to the guidance so that:
|
||||
|
||||
- Screenreader users are alerted to a new context -- the new question!
|
||||
- Keyboard users's focus is somewhere convenient to answer the question (but not _on_ an answer, since we don't want accidental submissions or for the user to hit enter straight and miss the guidance!)
|
||||
|
||||
-}
|
||||
guidanceId : String -> String
|
||||
guidanceId id_ =
|
||||
id_ ++ "__guidance-speech-bubble"
|
||||
|
||||
|
||||
{-|
|
||||
|
||||
QuestionBox.view
|
||||
[ QuestionBox.markdown "**WOW**, great component!"
|
||||
]
|
||||
|
||||
-}
|
||||
view : List (Attribute msg) -> Html msg
|
||||
view attributes =
|
||||
let
|
||||
config =
|
||||
List.foldl (\(Attribute f) a -> f a) defaultConfig attributes
|
||||
in
|
||||
case config.type_ of
|
||||
Standalone ->
|
||||
viewStandalone config
|
||||
|
||||
PointingTo element ->
|
||||
viewPointingTo config element
|
||||
|
||||
|
||||
{-| -}
|
||||
viewStandalone : Config msg -> Html msg
|
||||
viewStandalone config =
|
||||
div
|
||||
[ AttributesExtra.maybe Attributes.id config.id
|
||||
, css config.containerCss
|
||||
, nriDescription "standalone-balloon-container"
|
||||
]
|
||||
[ viewBalloon config
|
||||
[ Balloon.nriDescription "standalone-balloon"
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
{-| -}
|
||||
viewPointingTo : Config msg -> Maybe Element -> Html msg
|
||||
viewPointingTo config element =
|
||||
let
|
||||
xOffset =
|
||||
Maybe.map xOffsetPx element
|
||||
|> Maybe.withDefault 0
|
||||
in
|
||||
viewBalloon config
|
||||
[ Balloon.onBottom
|
||||
, Balloon.nriDescription "pointing-to-balloon"
|
||||
, case config.id of
|
||||
Just id_ ->
|
||||
Balloon.id id_
|
||||
|
||||
Nothing ->
|
||||
Balloon.css []
|
||||
, Balloon.containerCss
|
||||
[ Css.position Css.absolute
|
||||
, Css.top (Css.pct 100)
|
||||
, Css.left (Css.pct 50)
|
||||
, Css.transforms
|
||||
[ Css.translateX (Css.pct -50)
|
||||
, Css.translateY (Css.px 8)
|
||||
]
|
||||
, Css.minWidth (Css.px 300)
|
||||
, Css.textAlign Css.center
|
||||
, Css.batch config.containerCss
|
||||
]
|
||||
, Balloon.css <|
|
||||
if xOffset /= 0 then
|
||||
[ Css.property "transform" ("translateX(" ++ String.fromFloat xOffset ++ "px)")
|
||||
]
|
||||
|
||||
else
|
||||
[]
|
||||
]
|
||||
|
||||
|
||||
viewBalloon : Config msg -> List (Balloon.Attribute msg) -> Html msg
|
||||
viewBalloon config attributes =
|
||||
Balloon.view
|
||||
([ Balloon.html
|
||||
(List.filterMap identity
|
||||
[ Maybe.map (viewGuidance config) config.markdown
|
||||
, viewActions config.character config.actions
|
||||
]
|
||||
)
|
||||
, Balloon.customTheme { backgroundColor = Colors.glacier, color = Colors.glacier }
|
||||
, Balloon.css [ Css.padding (Css.px 0), Css.boxShadow Css.none ]
|
||||
]
|
||||
++ attributes
|
||||
)
|
||||
|
||||
|
||||
viewGuidance : { config | id : Maybe String, character : Maybe { name : String, icon : Svg } } -> String -> Html msg
|
||||
viewGuidance config markdown_ =
|
||||
case config.character of
|
||||
Just character_ ->
|
||||
div
|
||||
[ css
|
||||
[ Css.displayFlex
|
||||
, Css.justifyContent Css.flexEnd
|
||||
, Css.margin (Css.px 8)
|
||||
, Css.marginRight (Css.px 20)
|
||||
, Css.position Css.relative
|
||||
]
|
||||
]
|
||||
[ viewCharacter character_
|
||||
, viewSpeechBubble config
|
||||
[ Balloon.markdown markdown_
|
||||
, Balloon.onLeft
|
||||
, Balloon.alignArrowEnd
|
||||
, Balloon.css [ Css.minHeight (Css.px 46) ]
|
||||
]
|
||||
]
|
||||
|
||||
Nothing ->
|
||||
viewSpeechBubble config
|
||||
[ Balloon.markdown markdown_
|
||||
, Balloon.css [ Css.margin2 (Css.px 10) (Css.px 20) ]
|
||||
]
|
||||
|
||||
|
||||
viewSpeechBubble : { config | id : Maybe String } -> List (Balloon.Attribute msg) -> Html msg
|
||||
viewSpeechBubble config extraAttributes =
|
||||
Balloon.view
|
||||
([ Balloon.nriDescription "guidance-speech-bubble"
|
||||
, Balloon.white
|
||||
, Balloon.css
|
||||
[ Css.borderRadius (Css.px 16)
|
||||
, Css.padding (Css.px 10)
|
||||
, Css.boxShadow Css.none
|
||||
, Css.Global.children [ Css.Global.p [ Css.margin Css.zero ] ]
|
||||
]
|
||||
, Balloon.custom
|
||||
[ AttributesExtra.maybe (guidanceId >> Attributes.id) config.id
|
||||
, Key.tabbable False
|
||||
]
|
||||
]
|
||||
++ extraAttributes
|
||||
)
|
||||
|
||||
|
||||
viewCharacter : { name : String, icon : Svg } -> Html msg
|
||||
viewCharacter { name, icon } =
|
||||
icon
|
||||
|> Svg.withLabel (name ++ " says, ")
|
||||
|> Svg.withWidth (Css.px 50)
|
||||
|> Svg.withHeight (Css.px 70)
|
||||
|> Svg.withCss
|
||||
[ Css.position Css.absolute
|
||||
, Css.bottom (Css.px -18)
|
||||
, Css.right (Css.px -48)
|
||||
]
|
||||
|> Svg.toHtml
|
||||
|
||||
|
||||
viewActions : Maybe character -> List { label : String, onClick : msg } -> Maybe (Html msg)
|
||||
viewActions maybeCharacter actions_ =
|
||||
let
|
||||
containerStyles =
|
||||
[ Css.backgroundColor Colors.frost
|
||||
, Css.border3 (Css.px 1) Css.solid Colors.glacier
|
||||
, Css.borderBottomRightRadius (Css.px 8)
|
||||
, Css.borderBottomLeftRadius (Css.px 8)
|
||||
, Css.margin Css.zero
|
||||
, case maybeCharacter of
|
||||
Just _ ->
|
||||
Css.padding4 (Css.px 10) (Css.px 30) (Css.px 10) (Css.px 10)
|
||||
|
||||
Nothing ->
|
||||
Css.padding2 (Css.px 10) (Css.px 20)
|
||||
, Css.listStyle Css.none
|
||||
, Css.displayFlex
|
||||
, Css.property "gap" "10px"
|
||||
, Css.flexDirection Css.column
|
||||
]
|
||||
in
|
||||
case actions_ of
|
||||
[] ->
|
||||
Nothing
|
||||
|
||||
{ label, onClick } :: [] ->
|
||||
div [ css (Css.alignItems Css.center :: containerStyles) ]
|
||||
[ Button.button label
|
||||
[ Button.onClick onClick
|
||||
, Button.unboundedWidth
|
||||
, Button.small
|
||||
]
|
||||
]
|
||||
|> Just
|
||||
|
||||
_ ->
|
||||
ul [ css containerStyles ]
|
||||
(List.map
|
||||
(\{ label, onClick } ->
|
||||
li []
|
||||
[ Button.button label
|
||||
[ Button.onClick onClick
|
||||
, Button.fillContainerWidth
|
||||
, Button.small
|
||||
]
|
||||
]
|
||||
)
|
||||
actions_
|
||||
)
|
||||
|> Just
|
@ -7,6 +7,7 @@
|
||||
"Nri.Ui.AnimatedIcon.V1",
|
||||
"Nri.Ui.AssignmentIcon.V2",
|
||||
"Nri.Ui.Balloon.V2",
|
||||
"Nri.Ui.Block.V3",
|
||||
"Nri.Ui.Block.V4",
|
||||
"Nri.Ui.BreadCrumbs.V2",
|
||||
"Nri.Ui.Button.V10",
|
||||
@ -50,6 +51,7 @@
|
||||
"Nri.Ui.Panel.V1",
|
||||
"Nri.Ui.Pennant.V2",
|
||||
"Nri.Ui.PremiumCheckbox.V8",
|
||||
"Nri.Ui.QuestionBox.V2",
|
||||
"Nri.Ui.QuestionBox.V3",
|
||||
"Nri.Ui.RadioButton.V4",
|
||||
"Nri.Ui.RingGauge.V1",
|
||||
|
Loading…
Reference in New Issue
Block a user