Merge pull request #1337 from NoRedInk/tab-v8

This commit is contained in:
Brian Hicks 2023-03-30 12:57:40 -05:00 committed by GitHub
commit 987b8de5ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 549 additions and 34 deletions

View File

@ -58,7 +58,7 @@ Please link to any relevant context and stories.
- if you want to test the component directly, add tests under `tests/Spec`. Historically, this has been the more popular Elm testing strategy for noredink-ui.
- [ ] Component API follows standard patterns in noredink-ui
- e.g., as a dev, I can conveniently add an `nriDescription`
- and adding a new feature to the component will _not_ require major API changes to the comopnent
- and adding a new feature to the component will _not_ require major API changes to the component
- [ ] Please assign [team-accessibilibats-a11ybats](https://github.com/orgs/NoRedInk/teams/team-accessibilibats-a11ybats) as a reviewer in addition to assigning a reviewer from your team
@ -94,4 +94,4 @@ Please link to any relevant context and stories.
- [ ] Changes to the component are tested/the new version of the component is tested
- [ ] Component API follows standard patterns in noredink-ui
- e.g., as a dev, I can conveniently add an `nriDescription`
- and adding a new feature to the component will _not_ require major API changes to the comopnent
- and adding a new feature to the component will _not_ require major API changes to the component

View File

@ -1,7 +1,6 @@
module Code exposing
( string, stringMultiline, maybeString
, maybe
, maybeFloat
, bool
, commentInline
, list, listMultiline
@ -20,7 +19,6 @@ module Code exposing
@docs string, stringMultiline, maybeString
@docs maybe
@docs maybeFloat
@docs bool
@docs commentInline
@docs list, listMultiline
@ -66,12 +64,6 @@ maybeString =
maybe << Maybe.map string
{-| -}
maybeFloat : Maybe Float -> String
maybeFloat =
maybe << Maybe.map String.fromFloat
{-| -}
bool : Bool -> String
bool =

View File

@ -1,6 +1,6 @@
module Debug.Control.Extra exposing
( float, int
, list, listItem, optionalListItem, optionalListItemDefaultChecked
, values, list, listItem, optionalListItem, optionalListItemDefaultChecked
, optionalBoolListItem
, bool
, rotatedChoice, specificChoice
@ -9,7 +9,7 @@ module Debug.Control.Extra exposing
{-|
@docs float, int
@docs list, listItem, optionalListItem, optionalListItemDefaultChecked
@docs values, list, listItem, optionalListItem, optionalListItemDefaultChecked
@docs optionalBoolListItem
@docs bool
@docs rotatedChoice, specificChoice
@ -35,6 +35,14 @@ int default =
(Control.string (String.fromInt default))
{-| -}
values : (a -> String) -> List a -> Control a
values toString nums =
nums
|> List.map (\n -> ( toString n, Control.value n ))
|> Control.choice
{-| Use with `listItem` and `optionalListItem`
list

View File

@ -15,13 +15,14 @@ import Category exposing (Category(..))
import Code
import Css
import Debug.Control as Control exposing (Control)
import Debug.Control.Extra exposing (values)
import Debug.Control.View as ControlView
import Example exposing (Example)
import Html.Styled as Html
import Html.Styled.Attributes exposing (css)
import KeyboardSupport exposing (Key(..))
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.Tabs.V7 as Tabs exposing (Alignment(..), Tab)
import Nri.Ui.Tabs.V8 as Tabs exposing (Alignment(..), Tab)
import Nri.Ui.Text.V6 as Text
import Nri.Ui.Tooltip.V3 as Tooltip
import Task
@ -34,7 +35,7 @@ moduleName =
version : Int
version =
7
8
example : Example State Msg
@ -122,13 +123,35 @@ example =
let
code =
[ moduleName ++ ".view"
, " { title = " ++ Code.maybeString settings.title
, " , alignment = " ++ moduleName ++ "." ++ Debug.toString settings.alignment
, " , customSpacing = " ++ Code.maybeFloat settings.customSpacing
, " , focusAndSelect = identity"
, " { focusAndSelect = identity"
, " , selected = " ++ String.fromInt model.selected
, " , tabs = " ++ Code.listMultiline (List.map Tuple.first tabs) 2
, " }"
, Code.listMultiline
(List.filterMap identity
[ Just (moduleName ++ ".alignment " ++ moduleName ++ "." ++ Debug.toString settings.alignment)
, Maybe.map (\title -> moduleName ++ ".title " ++ Code.string title) settings.title
, Maybe.map (\spacing -> moduleName ++ ".spacing " ++ String.fromFloat spacing) settings.customSpacing
, Maybe.map (\color -> moduleName ++ ".pageBackgroundColor" ++ colorToCode color) settings.pageBackgroundColor
, Maybe.map
(\sticky ->
case sticky of
Default ->
moduleName ++ ".tabsListSticky"
Custom stickyConfig ->
moduleName
++ ".tabsListStickyCustom "
++ Code.recordMultiline
[ ( "topOffset", String.fromFloat stickyConfig.topOffset )
, ( "zIndex", String.fromInt stickyConfig.zIndex )
]
2
)
settings.stickiness
]
)
1
, Code.listMultiline (List.map Tuple.first tabs) 1
]
|> String.join "\n"
in
@ -138,13 +161,27 @@ example =
]
}
, Tabs.view
{ title = settings.title
, alignment = settings.alignment
, customSpacing = settings.customSpacing
, focusAndSelect = FocusAndSelectTab
{ focusAndSelect = FocusAndSelectTab
, selected = model.selected
, tabs = List.map Tuple.second tabs
}
(List.filterMap identity
[ Just (Tabs.alignment settings.alignment)
, Maybe.map Tabs.title settings.title
, Maybe.map Tabs.spacing settings.customSpacing
, Maybe.map (Tabs.pageBackgroundColor << colorToCss) settings.pageBackgroundColor
, Maybe.map
(\stickiness ->
case stickiness of
Default ->
Tabs.tabListSticky
Custom stickyConfig ->
Tabs.tabListStickyCustom stickyConfig
)
settings.stickiness
]
)
(List.map Tuple.second tabs)
]
}
@ -190,7 +227,13 @@ buildTooltip openTooltipId withTooltips id =
]
, Tabs.build { id = id, idString = tabIdString }
([ Tabs.tabString tabName
, Tabs.panelHtml (Html.text panelName)
, panelName
|> List.repeat 50
|> String.join "\n"
|> Html.text
|> List.singleton
|> Html.pre []
|> Tabs.panelHtml
]
++ (if withTooltips then
[ Tabs.withTooltip
@ -227,11 +270,50 @@ type alias Settings =
, alignment : Alignment
, customSpacing : Maybe Float
, withTooltips : Bool
, pageBackgroundColor : Maybe Color
, stickiness : Maybe Stickiness
}
type Color
= White
| Gray
colorToCss : Color -> Css.Color
colorToCss color =
case color of
White ->
Colors.white
Gray ->
Colors.gray92
colorToCode : Color -> String
colorToCode color =
case color of
White ->
"Colors.white"
Gray ->
"Colors.gray92"
type Stickiness
= Default
| Custom Tabs.TabListStickyConfig
initSettings : Control Settings
initSettings =
let
colorChoices =
Control.choice
[ ( "Gray", Control.value Gray )
, ( "White", Control.value White )
]
in
Control.record Settings
|> Control.field "title" (Control.maybe False (Control.string "Title"))
|> Control.field "alignment"
@ -241,18 +323,22 @@ initSettings =
, ( "Right", Control.value Right )
]
)
|> Control.field "customSpacing"
|> Control.field "customSpacing" (Control.maybe False (values String.fromFloat [ 2, 3, 4, 8, 16 ]))
|> Control.field "withTooltips" (Control.bool True)
|> Control.field "pageBackgroundColor" (Control.maybe False colorChoices)
|> Control.field "tabListSticky"
(Control.maybe False
(Control.choice
[ ( "2", Control.value 2 )
, ( "3", Control.value 3 )
, ( "4", Control.value 4 )
, ( "8", Control.value 8 )
, ( "16", Control.value 16 )
[ ( "Default", Control.value Default )
, ( "Custom"
, Control.record Tabs.TabListStickyConfig
|> Control.field "topOffset" (values String.fromFloat [ 0, 10, 50 ])
|> Control.field "zIndex" (values String.fromInt [ 0, 1, 5, 10 ])
|> Control.map Custom
)
]
)
)
|> Control.field "withTooltips" (Control.bool True)
type Msg

View File

@ -6,4 +6,5 @@ Nri.Ui.HighlighterToolbar.V2,upgrade to V3
Nri.Ui.QuestionBox.V2,upgrade to V4
Nri.Ui.QuestionBox.V3,upgrade to V4
Nri.Ui.Select.V8,upgrade to V9
Nri.Ui.Tabs.V6,upgrade to V7
Nri.Ui.Tabs.V6,upgrade to V8
Nri.Ui.Tabs.V7,upgrade to V8

1 Nri.Ui.Block.V3 upgrade to V4
6 Nri.Ui.QuestionBox.V2 upgrade to V4
7 Nri.Ui.QuestionBox.V3 upgrade to V4
8 Nri.Ui.Select.V8 upgrade to V9
9 Nri.Ui.Tabs.V6 upgrade to V7 upgrade to V8
10 Nri.Ui.Tabs.V7 upgrade to V8

View File

@ -77,6 +77,7 @@
"Nri.Ui.Table.V6",
"Nri.Ui.Tabs.V6",
"Nri.Ui.Tabs.V7",
"Nri.Ui.Tabs.V8",
"Nri.Ui.Text.V6",
"Nri.Ui.Text.Writing.V1",
"Nri.Ui.TextArea.V5",

View File

@ -198,7 +198,10 @@ hint = 'upgrade to V5'
hint = 'upgrade to V6'
[forbidden."Nri.Ui.Tabs.V6"]
hint = 'upgrade to V7'
hint = 'upgrade to V8'
[forbidden."Nri.Ui.Tabs.V7"]
hint = 'upgrade to V8'
[forbidden."Nri.Ui.Text.V2"]
hint = 'upgrade to V5'

423
src/Nri/Ui/Tabs/V8.elm Normal file
View File

@ -0,0 +1,423 @@
module Nri.Ui.Tabs.V8 exposing
( Attribute, title, spacing
, Alignment(..), alignment
, pageBackgroundColor
, tabListSticky, TabListStickyConfig, tabListStickyCustom
, view
, Tab, TabAttribute, build
, tabString, tabHtml, withTooltip, disabled, labelledBy, describedBy
, panelHtml
, spaHref
)
{-| Changes from V7:
- Uses an HTML-like API
- Adds sticky positioning
- Adds background color in the tab list (for use with sticky positioning)
### Attributes
@docs Attribute, title, spacing
@docs Alignment, alignment
@docs pageBackgroundColor
@docs tabListSticky, TabListStickyConfig, tabListStickyCustom
@docs view
### Tabs
@docs Tab, TabAttribute, build
@docs tabString, tabHtml, withTooltip, disabled, labelledBy, describedBy
@docs panelHtml
@docs spaHref
-}
import Css exposing (..)
import Css.Media
import Html.Styled as Html exposing (Html)
import Html.Styled.Attributes as Attributes
import Nri.Ui
import Nri.Ui.Colors.Extra exposing (withAlpha)
import Nri.Ui.Colors.V1 as Colors
import Nri.Ui.FocusRing.V1 as FocusRing
import Nri.Ui.Fonts.V1 as Fonts
import Nri.Ui.MediaQuery.V1 as MediaQuery
import Nri.Ui.Tooltip.V3 as Tooltip
import TabsInternal.V2 as TabsInternal
{-| -}
type Tab id msg
= Tab (TabsInternal.Tab id msg)
{-| -}
type TabAttribute id msg
= TabAttribute (TabsInternal.Tab id msg -> TabsInternal.Tab id msg)
{-| -}
tabString : String -> TabAttribute id msg
tabString content =
TabAttribute (\tab -> { tab | tabView = [ viewTabDefault content ] })
{-| -}
tabHtml : Html Never -> TabAttribute id msg
tabHtml content =
TabAttribute (\tab -> { tab | tabView = [ Html.map never content ] })
{-| Tooltip defaults: `[Tooltip.smallPadding, Tooltip.onBottom, Tooltip.fitToContent]`
-}
withTooltip : List (Tooltip.Attribute msg) -> TabAttribute id msg
withTooltip attributes =
TabAttribute (\tab -> { tab | tabTooltip = attributes })
{-| Makes it so that the tab can't be clicked or focused via keyboard navigation
-}
disabled : Bool -> TabAttribute id msg
disabled isDisabled =
TabAttribute (\tab -> { tab | disabled = isDisabled })
{-| Sets an overriding labelledBy on the tab for an external tooltip.
This assumes an external tooltip is set and disables any internal tooltip configured.
-}
labelledBy : String -> TabAttribute id msg
labelledBy labelledById =
TabAttribute (\tab -> { tab | labelledBy = Just labelledById })
{-| Like [`labelledBy`](#labelledBy), but it describes the given element
instead of labeling it.
This attribute can be used multiple times if more than one element describes
this tab.
-}
describedBy : String -> TabAttribute id msg
describedBy describedById =
TabAttribute (\tab -> { tab | describedBy = describedById :: tab.describedBy })
{-| -}
panelHtml : Html msg -> TabAttribute id msg
panelHtml content =
TabAttribute (\tab -> { tab | panelView = content })
{-| -}
spaHref : String -> TabAttribute id msg
spaHref url =
TabAttribute (\tab -> { tab | spaHref = Just url })
{-| -}
tabAttributes : List (Html.Attribute msg) -> TabAttribute id msg
tabAttributes attrs =
TabAttribute (\tab -> { tab | tabAttributes = tab.tabAttributes ++ attrs })
{-| -}
build : { id : id, idString : String } -> List (TabAttribute id msg) -> Tab id msg
build config attributes =
Tab
(TabsInternal.fromList config
(List.map (\(TabAttribute f) -> f)
(tabAttributes [ Attributes.class FocusRing.customClass ]
:: attributes
)
)
)
{-| Determines whether tabs are centered or floating to the left or right.
-}
type Alignment
= Left
| Center
| Right
{-| Ways to adapt the appearance of the tabs to your application.
-}
type Attribute id msg
= Attribute (Config -> Config)
{-| Set a title in the tab list.
-}
title : String -> Attribute id msg
title title_ =
Attribute (\config -> { config | title = Just title_ })
{-| Set the alignment of the tab list.
-}
alignment : Alignment -> Attribute id msg
alignment alignment_ =
Attribute (\config -> { config | alignment = alignment_ })
{-| Set the spacing between tabs in the tab list.
-}
spacing : Float -> Attribute id msg
spacing spacing_ =
Attribute (\config -> { config | spacing = Just spacing_ })
{-| Tell this tab list about the background color of the page it lievs on. This
is mostly useful when setting up sticky headers, to prevent page content from
showing through the background.
-}
pageBackgroundColor : Css.Color -> Attribute id msg
pageBackgroundColor color =
Attribute (\config -> { config | pageBackgroundColor = Just color })
{-| Make the tab list sticky. You probably want to set an explicit background
color along with this!
-}
tabListSticky : Attribute id msg
tabListSticky =
Attribute (\config -> { config | tabListStickyConfig = Just defaultTabListStickyConfig })
{-| Make the tab list sticky, overriding the default behavior. You should
probably set an explicit background color along with this.
-}
tabListStickyCustom : TabListStickyConfig -> Attribute id msg
tabListStickyCustom custom =
Attribute (\config -> { config | tabListStickyConfig = Just custom })
type alias Config =
{ title : Maybe String
, alignment : Alignment
, spacing : Maybe Float
, pageBackgroundColor : Maybe Css.Color
, tabListStickyConfig : Maybe TabListStickyConfig
}
defaultConfig : Config
defaultConfig =
{ title = Nothing
, alignment = Left
, spacing = Nothing
, pageBackgroundColor = Nothing
, tabListStickyConfig = Nothing
}
{-| Configure how the top bar is sticky.
- `topOffset` controls how far from the top of the viewport the bar will
stick, in pixels. (**Default value:** 0)
- `zIndex` controls how high up the z-order the bar will float. (**Default
value:** 0)
-}
type alias TabListStickyConfig =
{ topOffset : Float
, zIndex : Int
}
defaultTabListStickyConfig : TabListStickyConfig
defaultTabListStickyConfig =
{ topOffset = 0
, zIndex = 0
}
{-| -}
view :
{ focusAndSelect : { select : id, focus : Maybe String } -> msg
, selected : id
}
-> List (Attribute id msg)
-> List (Tab id msg)
-> Html msg
view { focusAndSelect, selected } attrs tabs =
let
config =
List.foldl (\(Attribute fn) soFar -> fn soFar) defaultConfig attrs
{ tabList, tabPanels } =
TabsInternal.views
{ focusAndSelect = focusAndSelect
, selected = selected
, tabs = List.map (\(Tab t) -> t) tabs
, tabStyles = tabStyles config.spacing
, tabListStyles = stylesTabsAligned config
}
in
Nri.Ui.styled Html.div
"Nri-Ui-Tabs__container"
[]
[]
[ Html.styled Html.div
[ Css.displayFlex
, Css.alignItems Css.flexEnd
, Css.borderBottom (Css.px 1)
, Css.borderBottomStyle Css.solid
, Css.borderBottomColor Colors.navy
, Fonts.baseFont
, maybeStyle
(\{ topOffset, zIndex } ->
Css.Media.withMedia
[ MediaQuery.notMobile ]
[ Css.position Css.sticky
, Css.top (Css.px topOffset)
, Css.zIndex (Css.int zIndex)
]
)
config.tabListStickyConfig
, maybeStyle Css.backgroundColor config.pageBackgroundColor
]
[]
[ config.title
|> Maybe.map viewTitle
|> Maybe.withDefault (Html.text "")
, tabList
]
, tabPanels
]
{-| -}
viewTabDefault : String -> Html msg
viewTabDefault tabTitle =
Html.div
[ Attributes.css
[ Css.padding4 (Css.px 14) (Css.px 20) (Css.px 12) (Css.px 20)
]
]
[ Html.text tabTitle ]
viewTitle : String -> Html msg
viewTitle tabTitle =
Html.styled Html.h1
[ Css.flexGrow (Css.int 2)
, Css.fontSize (Css.px 20)
, Css.fontWeight Css.bold
, Css.margin4 (Css.px 5) (Css.px 10) (Css.px 10) Css.zero
, Css.color Colors.navy
]
[]
[ Html.text tabTitle ]
-- STYLES
stylesTabsAligned : Config -> List Style
stylesTabsAligned config =
let
alignmentStyles =
case config.alignment of
Left ->
Css.justifyContent Css.flexStart
Center ->
Css.justifyContent Css.center
Right ->
Css.justifyContent Css.flexEnd
in
[ alignmentStyles
, Css.margin Css.zero
, Css.fontSize (Css.px 19)
, Css.displayFlex
, Css.flexGrow (Css.int 1)
, Css.padding Css.zero
]
maybeStyle : (a -> Style) -> Maybe a -> Style
maybeStyle styler maybeValue =
case maybeValue of
Just value ->
styler value
Nothing ->
Css.batch []
tabStyles : Maybe Float -> Int -> Bool -> List Style
tabStyles customSpacing index isSelected =
let
stylesDynamic =
if isSelected then
[ Css.backgroundColor Colors.white
, Css.borderBottom (Css.px 1)
, Css.borderBottomStyle Css.solid
, Css.borderBottomColor Colors.white
]
else
[ Css.backgroundColor Colors.frost
, Css.backgroundImage <|
Css.linearGradient2 Css.toTop
(Css.stop2 (withAlpha 0.25 Colors.azure) (Css.pct 0))
(Css.stop2 (withAlpha 0 Colors.azure) (Css.pct 25))
[ Css.stop2 (withAlpha 0 Colors.azure) (Css.pct 100) ]
]
baseStyles =
[ Css.color Colors.navy
, Css.position Css.relative
-- necessary because bourbon or bootstrap or whatever add underlines when tabs are used as links
, Css.textDecoration Css.none |> important
, Css.property "background" "none"
, Css.fontFamily Css.inherit
, Css.fontSize Css.inherit
, Css.cursor Css.pointer
, Css.border zero
, Css.height (Css.pct 100)
]
stylesTab =
[ Css.display Css.inlineBlock
, Css.borderTopLeftRadius (Css.px 10)
, Css.borderTopRightRadius (Css.px 10)
, Css.border3 (Css.px 1) Css.solid Colors.navy
, Css.marginTop Css.zero
, Css.marginLeft
(if index == 0 then
Css.px 0
else
Css.px margin
)
, Css.marginRight (Css.px margin)
, Css.padding2 (Css.px 1) (Css.px 6)
, Css.marginBottom (Css.px -1)
, Css.cursor Css.pointer
, property "transition" "background-color 0.2s"
, property "transition" "border-color 0.2s"
, hover
[ backgroundColor Colors.white
, borderTopColor Colors.azure
, borderRightColor Colors.azure
, borderLeftColor Colors.azure
]
, pseudoClass "focus-visible"
[ FocusRing.outerBoxShadow
, Css.outline3 (Css.px 2) Css.solid Css.transparent
]
]
margin =
Maybe.withDefault 10 customSpacing / 2
in
baseStyles ++ stylesTab ++ stylesDynamic

View File

@ -73,6 +73,7 @@
"Nri.Ui.Table.V6",
"Nri.Ui.Tabs.V6",
"Nri.Ui.Tabs.V7",
"Nri.Ui.Tabs.V8",
"Nri.Ui.Text.V6",
"Nri.Ui.Text.Writing.V1",
"Nri.Ui.TextArea.V5",