mirror of
https://github.com/NoRedInk/noredink-ui.git
synced 2024-11-05 08:34:38 +03:00
Merge pull request #1338 from NoRedInk/table-sticky-header
add sticky headers to sortable tables
This commit is contained in:
commit
ace1f19247
@ -19,7 +19,7 @@ import Nri.Ui.AnimatedIcon.V1 as AnimatedIcon
|
||||
import Nri.Ui.Colors.V1 as Colors
|
||||
import Nri.Ui.Heading.V3 as Heading
|
||||
import Nri.Ui.Svg.V1 as Svg
|
||||
import Nri.Ui.Table.V6 as Table
|
||||
import Nri.Ui.Table.V7 as Table
|
||||
|
||||
|
||||
moduleName : String
|
||||
@ -88,7 +88,7 @@ example =
|
||||
]
|
||||
}
|
||||
, Heading.h2 [ Heading.plaintext "Example" ]
|
||||
, Table.view
|
||||
, Table.view []
|
||||
[ Table.custom
|
||||
{ header = text "Rendered"
|
||||
, view =
|
||||
|
@ -26,7 +26,7 @@ import Nri.Ui.ClickableText.V3 as ClickableText
|
||||
import Nri.Ui.Fonts.V1 as Fonts
|
||||
import Nri.Ui.Heading.V3 as Heading
|
||||
import Nri.Ui.Spacing.V1 as Spacing
|
||||
import Nri.Ui.Table.V6 as Table
|
||||
import Nri.Ui.Table.V7 as Table
|
||||
import Nri.Ui.Text.V6 as Text
|
||||
import Nri.Ui.UiIcon.V1 as UiIcon
|
||||
import Task
|
||||
@ -251,7 +251,7 @@ example =
|
||||
, Block.labelPosition (Dict.get longId offsets)
|
||||
]
|
||||
]
|
||||
, Table.view
|
||||
, Table.view []
|
||||
[ Table.custom
|
||||
{ header = text "Pattern name & description"
|
||||
, view = .description >> Markdown.toHtml Nothing >> List.map fromUnstyled >> div []
|
||||
|
@ -23,7 +23,7 @@ import Nri.Ui.Fonts.V1 as Fonts
|
||||
import Nri.Ui.Heading.V3 as Heading
|
||||
import Nri.Ui.Html.V3 exposing (viewJust)
|
||||
import Nri.Ui.Svg.V1 as Svg exposing (Svg)
|
||||
import Nri.Ui.Table.V6 as Table
|
||||
import Nri.Ui.Table.V7 as Table
|
||||
import Nri.Ui.UiIcon.V1 as UiIcon
|
||||
|
||||
|
||||
@ -173,7 +173,7 @@ example =
|
||||
[ Heading.h2 [ Heading.plaintext "viewSecondary Example" ]
|
||||
, viewJust (Tuple.second >> viewSecondaryExample settings.currentRoute) breadCrumbs
|
||||
]
|
||||
, Table.view
|
||||
, Table.view []
|
||||
[ Table.string
|
||||
{ header = "Name"
|
||||
, value = .name
|
||||
|
@ -21,7 +21,7 @@ import Nri.Ui.Colors.V1 as Colors
|
||||
import Nri.Ui.Fonts.V1 as Fonts
|
||||
import Nri.Ui.Heading.V3 as Heading
|
||||
import Nri.Ui.Svg.V1 as Svg
|
||||
import Nri.Ui.Table.V6 as Table
|
||||
import Nri.Ui.Table.V7 as Table
|
||||
|
||||
|
||||
moduleName : String
|
||||
@ -66,7 +66,7 @@ example =
|
||||
, Heading.h2 [ Heading.plaintext "Customizable example" ]
|
||||
, exampleView
|
||||
, Heading.h2 [ Heading.plaintext "Examples" ]
|
||||
, Table.view
|
||||
, Table.view []
|
||||
[ Table.string
|
||||
{ header = "State"
|
||||
, value = .state
|
||||
|
@ -13,7 +13,7 @@ import Html.Styled as Html exposing (Html)
|
||||
import Html.Styled.Attributes exposing (css)
|
||||
import Nri.Ui.ClickableText.V3 as ClickableText
|
||||
import Nri.Ui.Fonts.V1 as Fonts
|
||||
import Nri.Ui.Table.V6 as Table
|
||||
import Nri.Ui.Table.V7 as Table
|
||||
import Nri.Ui.Text.V6 as Text
|
||||
|
||||
|
||||
@ -90,7 +90,7 @@ viewFontFailurePatterns =
|
||||
, Css.textAlign Css.center
|
||||
]
|
||||
in
|
||||
Table.view
|
||||
Table.view []
|
||||
[ Table.rowHeader
|
||||
{ header = Html.text "Example"
|
||||
, view = Html.text << .example
|
||||
|
@ -25,7 +25,7 @@ import Nri.Ui.Heading.V3 as Heading
|
||||
import Nri.Ui.Highlightable.V2 as Highlightable exposing (Highlightable)
|
||||
import Nri.Ui.Highlighter.V3 as Highlighter
|
||||
import Nri.Ui.HighlighterTool.V1 as Tool
|
||||
import Nri.Ui.Table.V6 as Table
|
||||
import Nri.Ui.Table.V7 as Table
|
||||
import String.Extra
|
||||
|
||||
|
||||
@ -113,7 +113,7 @@ example =
|
||||
]
|
||||
, Heading.h2 [ Heading.plaintext "Non-interactive examples" ]
|
||||
, Heading.h3 [ Heading.plaintext "These are examples of some different ways the highlighter can appear to users." ]
|
||||
, Table.view
|
||||
, Table.view []
|
||||
[ Table.rowHeader
|
||||
{ header = text "Highlighter."
|
||||
, view = .viewName >> text
|
||||
|
@ -29,7 +29,7 @@ import Nri.Ui.Fonts.V1 as Fonts
|
||||
import Nri.Ui.Heading.V3 as Heading
|
||||
import Nri.Ui.Menu.V4 as Menu
|
||||
import Nri.Ui.Spacing.V1 as Spacing
|
||||
import Nri.Ui.Table.V6 as Table
|
||||
import Nri.Ui.Table.V7 as Table
|
||||
import Nri.Ui.TextInput.V7 as TextInput
|
||||
import Nri.Ui.Tooltip.V3 as Tooltip
|
||||
import Nri.Ui.UiIcon.V1 as UiIcon
|
||||
@ -230,7 +230,7 @@ view ellieLinkConfig state =
|
||||
[ Heading.plaintext "Menu types"
|
||||
, Heading.css [ Css.margin2 Spacing.verticalSpacerPx Css.zero ]
|
||||
]
|
||||
, Table.view
|
||||
, Table.view []
|
||||
[ Table.string
|
||||
{ header = "Menu type"
|
||||
, value = .menu
|
||||
|
@ -33,7 +33,7 @@ import Nri.Ui.MediaQuery.V1 exposing (..)
|
||||
import Nri.Ui.QuestionBox.V4 as QuestionBox
|
||||
import Nri.Ui.Spacing.V1 as Spacing
|
||||
import Nri.Ui.Svg.V1 as Svg
|
||||
import Nri.Ui.Table.V6 as Table
|
||||
import Nri.Ui.Table.V7 as Table
|
||||
import Nri.Ui.Text.V6 as Text
|
||||
import Nri.Ui.UiIcon.V1 as UiIcon
|
||||
import Task
|
||||
@ -238,7 +238,7 @@ While these visions did appear.
|
||||
]
|
||||
]
|
||||
]
|
||||
, Table.view
|
||||
, Table.view []
|
||||
[ Table.custom
|
||||
{ header = text "Pattern name & description"
|
||||
, view = .description >> Markdown.toHtml Nothing >> List.map fromUnstyled >> div []
|
||||
|
@ -20,7 +20,7 @@ import Nri.Ui.Colors.V1 as Colors
|
||||
import Nri.Ui.Heading.V3 as Heading
|
||||
import Nri.Ui.RingGauge.V1 as RingGauge
|
||||
import Nri.Ui.Svg.V1 as Svg
|
||||
import Nri.Ui.Table.V6 as Table
|
||||
import Nri.Ui.Table.V7 as Table
|
||||
import Round
|
||||
import SolidColor.Accessibility
|
||||
|
||||
@ -103,7 +103,7 @@ example =
|
||||
|> Svg.withWidth (Css.px 200)
|
||||
|> Svg.withHeight (Css.px 200)
|
||||
|> Svg.toHtml
|
||||
, Table.view
|
||||
, Table.view []
|
||||
[ Table.string
|
||||
{ header = "Color contrast against"
|
||||
, value = .name
|
||||
|
@ -10,16 +10,16 @@ import Category exposing (Category(..))
|
||||
import Code
|
||||
import Css exposing (..)
|
||||
import Debug.Control as Control exposing (Control)
|
||||
import Debug.Control.Extra as ControlExtra
|
||||
import Debug.Control.Extra as ControlExtra exposing (values)
|
||||
import Debug.Control.View as ControlView
|
||||
import Example exposing (Example)
|
||||
import Html.Styled as Html exposing (..)
|
||||
import Html.Styled.Attributes exposing (css)
|
||||
import Nri.Ui.Colors.V1 as Colors
|
||||
import Nri.Ui.Heading.V3 as Heading
|
||||
import Nri.Ui.SortableTable.V3 as SortableTable exposing (Column)
|
||||
import Nri.Ui.SortableTable.V4 as SortableTable exposing (Column)
|
||||
import Nri.Ui.Svg.V1 as Svg exposing (Svg)
|
||||
import Nri.Ui.Table.V6 as Table
|
||||
import Nri.Ui.Table.V7 as Table
|
||||
import Nri.Ui.UiIcon.V1 as UiIcon
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ moduleName =
|
||||
|
||||
version : Int
|
||||
version =
|
||||
3
|
||||
4
|
||||
|
||||
|
||||
{-| -}
|
||||
@ -74,7 +74,7 @@ example =
|
||||
|> Svg.withHeight (Css.px 12)
|
||||
|> Svg.toHtml
|
||||
in
|
||||
[ Table.view
|
||||
[ Table.view []
|
||||
[ Table.custom
|
||||
{ header = header "X"
|
||||
, view = .x >> Html.text
|
||||
@ -104,10 +104,24 @@ example =
|
||||
settings =
|
||||
Control.currentValue model.settings
|
||||
|
||||
config =
|
||||
{ updateMsg = SetSortState
|
||||
, columns = columns
|
||||
}
|
||||
attrs =
|
||||
List.filterMap identity
|
||||
[ Just (SortableTable.updateMsg SetSortState)
|
||||
, Just (SortableTable.state sortState)
|
||||
, Maybe.map
|
||||
(\stickiness ->
|
||||
case stickiness of
|
||||
Default ->
|
||||
SortableTable.stickyHeader
|
||||
|
||||
Custom customConfig ->
|
||||
SortableTable.stickyHeaderCustom customConfig
|
||||
)
|
||||
settings.stickyHeader
|
||||
]
|
||||
|
||||
isStickyAtAll =
|
||||
settings.stickyHeader /= Nothing
|
||||
|
||||
( dataCode, data ) =
|
||||
List.unzip dataWithCode
|
||||
@ -119,15 +133,35 @@ example =
|
||||
{ sectionName = viewName
|
||||
, code =
|
||||
[ moduleName ++ "." ++ viewName
|
||||
, Code.recordMultiline
|
||||
[ ( "updateMsg", "SetSortState" )
|
||||
, ( "columns", Code.listMultiline columnsCode 2 )
|
||||
]
|
||||
, Code.listMultiline
|
||||
(List.filterMap identity
|
||||
[ Just "SortableTable.updateMsg SetSortState"
|
||||
, "-- The SortableTable's state should be stored on the model, rather than initialized in the view"
|
||||
++ "\n "
|
||||
++ "SortableTable.state (SortableTable.init "
|
||||
++ Debug.toString model.sortState.column
|
||||
++ ")"
|
||||
|> Just
|
||||
, Maybe.map
|
||||
(\stickiness ->
|
||||
case stickiness of
|
||||
Default ->
|
||||
"SortableTable.stickyHeader"
|
||||
|
||||
Custom stickyConfig ->
|
||||
"SortableTable.stickyHeaderCustom "
|
||||
++ Code.recordMultiline
|
||||
[ ( "topOffset", String.fromFloat stickyConfig.topOffset )
|
||||
, ( "zIndex", String.fromInt stickyConfig.zIndex )
|
||||
, ( "pageBackgroundColor", "Css.hex \"" ++ stickyConfig.pageBackgroundColor.value ++ "\"" )
|
||||
]
|
||||
2
|
||||
)
|
||||
settings.stickyHeader
|
||||
]
|
||||
)
|
||||
1
|
||||
, Code.newlineWithIndent 1
|
||||
, Code.commentInline "The SortableTable's state should be stored on the model, rather than initialized in the view"
|
||||
, Code.newlineWithIndent 1
|
||||
, Code.withParens ("SortableTable.init " ++ Debug.toString model.sortState.column)
|
||||
, Code.listMultiline columnsCode 1
|
||||
, finalArgs
|
||||
]
|
||||
|> String.join ""
|
||||
@ -149,10 +183,19 @@ example =
|
||||
}
|
||||
, Heading.h2 [ Heading.plaintext "Example" ]
|
||||
, if settings.loading then
|
||||
SortableTable.viewLoading config sortState
|
||||
SortableTable.viewLoading attrs columns
|
||||
|
||||
else
|
||||
SortableTable.view config sortState data
|
||||
SortableTable.view attrs
|
||||
columns
|
||||
(if isStickyAtAll then
|
||||
data
|
||||
|> List.repeat 10
|
||||
|> List.concat
|
||||
|
||||
else
|
||||
data
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
@ -274,9 +317,15 @@ type alias Settings =
|
||||
, customizableColumnWidth : Int
|
||||
, customizableColumnCellStyles : ( String, List Style )
|
||||
, loading : Bool
|
||||
, stickyHeader : Maybe StickyHeader
|
||||
}
|
||||
|
||||
|
||||
type StickyHeader
|
||||
= Default
|
||||
| Custom SortableTable.StickyConfig
|
||||
|
||||
|
||||
controlSettings : Control Settings
|
||||
controlSettings =
|
||||
Control.record Settings
|
||||
@ -295,6 +344,25 @@ controlSettings =
|
||||
]
|
||||
)
|
||||
|> Control.field "Is loading" (Control.bool False)
|
||||
|> Control.field "Sticky header"
|
||||
(Control.maybe False
|
||||
(Control.choice
|
||||
[ ( "Default", Control.value Default )
|
||||
, ( "Custom"
|
||||
, Control.record SortableTable.StickyConfig
|
||||
|> Control.field "topOffset" (values String.fromFloat [ 0, 10, 50 ])
|
||||
|> Control.field "zIndex" (values String.fromInt [ 0, 1, 5, 10 ])
|
||||
|> Control.field "pageBackgroundColor"
|
||||
(Control.choice
|
||||
[ ( "white", Control.value Colors.white )
|
||||
, ( "gray", Control.value Colors.gray92 )
|
||||
]
|
||||
)
|
||||
|> Control.map Custom
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
type ColumnId
|
||||
|
@ -21,7 +21,7 @@ import Nri.Ui.Container.V2 as Container
|
||||
import Nri.Ui.Fonts.V1 as Fonts
|
||||
import Nri.Ui.Heading.V3 as Heading
|
||||
import Nri.Ui.Spacing.V1 as Spacing
|
||||
import Nri.Ui.Table.V6 as Table
|
||||
import Nri.Ui.Table.V7 as Table
|
||||
import Svg.Styled
|
||||
import Svg.Styled.Attributes
|
||||
|
||||
@ -237,7 +237,7 @@ view ellieLinkConfig state =
|
||||
, Heading.h2 [ Heading.plaintext "Example", Heading.css [ Css.marginTop Spacing.verticalSpacerPx ] ]
|
||||
, fakePage [ exampleView ]
|
||||
, Heading.h2 [ Heading.plaintext "Content alignment", Heading.css [ Css.marginTop Spacing.verticalSpacerPx ] ]
|
||||
, Table.view
|
||||
, Table.view []
|
||||
[ Table.string
|
||||
{ header = "Name"
|
||||
, value = .name
|
||||
@ -275,7 +275,7 @@ view ellieLinkConfig state =
|
||||
, { name = "centeredContentWithCustomWidth", alignment = "Centered", maxWidth = "(customizable)", sidePadding = "0px" }
|
||||
]
|
||||
, Heading.h2 [ Heading.plaintext "Constants", Heading.css [ Css.marginTop Spacing.verticalSpacerPx ] ]
|
||||
, Table.view
|
||||
, Table.view []
|
||||
[ Table.string
|
||||
{ header = "Name"
|
||||
, value = .name
|
||||
|
@ -15,7 +15,7 @@ import Debug.Control.View as ControlView
|
||||
import Example exposing (Example)
|
||||
import Nri.Ui.Button.V10 as Button
|
||||
import Nri.Ui.Heading.V3 as Heading
|
||||
import Nri.Ui.Table.V6 as Table exposing (Column)
|
||||
import Nri.Ui.Table.V7 as Table exposing (Column)
|
||||
|
||||
|
||||
{-| -}
|
||||
@ -30,7 +30,7 @@ moduleName =
|
||||
|
||||
version : Int
|
||||
version =
|
||||
6
|
||||
7
|
||||
|
||||
|
||||
{-| -}
|
||||
@ -44,7 +44,7 @@ example =
|
||||
, categories = [ Layout ]
|
||||
, keyboardSupport = []
|
||||
, preview =
|
||||
[ Table.view
|
||||
[ Table.view []
|
||||
[ Table.string
|
||||
{ header = "A"
|
||||
, value = .a
|
||||
@ -98,6 +98,7 @@ example =
|
||||
{ sectionName = moduleName ++ "." ++ viewName
|
||||
, code =
|
||||
(moduleName ++ "." ++ viewName)
|
||||
++ " [] "
|
||||
++ Code.list columnsCode
|
||||
++ dataStr
|
||||
}
|
||||
@ -111,16 +112,16 @@ example =
|
||||
, Heading.h2 [ Heading.plaintext "Example" ]
|
||||
, case ( showHeader, isLoading ) of
|
||||
( True, False ) ->
|
||||
Table.view columns data
|
||||
Table.view [] columns data
|
||||
|
||||
( False, False ) ->
|
||||
Table.viewWithoutHeader columns data
|
||||
Table.viewWithoutHeader [] columns data
|
||||
|
||||
( True, True ) ->
|
||||
Table.viewLoading columns
|
||||
Table.viewLoading [] columns
|
||||
|
||||
( False, True ) ->
|
||||
Table.viewLoadingWithoutHeader columns
|
||||
Table.viewLoadingWithoutHeader [] columns
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ import Nri.Ui.ClickableText.V3 as ClickableText
|
||||
import Nri.Ui.Colors.V1 as Colors
|
||||
import Nri.Ui.Heading.V3 as Heading
|
||||
import Nri.Ui.Svg.V1 as Svg
|
||||
import Nri.Ui.Table.V6 as Table
|
||||
import Nri.Ui.Table.V7 as Table
|
||||
import Nri.Ui.Tooltip.V3 as Tooltip
|
||||
import Nri.Ui.UiIcon.V1 as UiIcon
|
||||
|
||||
@ -136,7 +136,7 @@ view : EllieLink.Config -> State -> List (Html Msg)
|
||||
view ellieLinkConfig model =
|
||||
[ viewCustomizableExample ellieLinkConfig model.staticExampleSettings
|
||||
, Heading.h2 [ Heading.plaintext "What type of tooltip should I use?" ]
|
||||
, Table.view
|
||||
, Table.view []
|
||||
[ Table.string
|
||||
{ header = "Type"
|
||||
, value = .name
|
||||
|
@ -6,5 +6,7 @@ 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.SortableTable.V3,upgrade to V4
|
||||
Nri.Ui.Table.V6,upgrade to V7
|
||||
Nri.Ui.Tabs.V6,upgrade to V8
|
||||
Nri.Ui.Tabs.V7,upgrade to V8
|
||||
|
|
2
elm.json
2
elm.json
@ -70,11 +70,13 @@
|
||||
"Nri.Ui.Shadows.V1",
|
||||
"Nri.Ui.SideNav.V4",
|
||||
"Nri.Ui.SortableTable.V3",
|
||||
"Nri.Ui.SortableTable.V4",
|
||||
"Nri.Ui.Spacing.V1",
|
||||
"Nri.Ui.Sprite.V1",
|
||||
"Nri.Ui.Svg.V1",
|
||||
"Nri.Ui.Switch.V2",
|
||||
"Nri.Ui.Table.V6",
|
||||
"Nri.Ui.Table.V7",
|
||||
"Nri.Ui.Tabs.V6",
|
||||
"Nri.Ui.Tabs.V7",
|
||||
"Nri.Ui.Tabs.V8",
|
||||
|
@ -188,6 +188,9 @@ hint = 'upgrade to V4'
|
||||
[forbidden."Nri.Ui.SortableTable.V2"]
|
||||
hint = 'upgrade to V3'
|
||||
|
||||
[forbidden."Nri.Ui.SortableTable.V3"]
|
||||
hint = 'upgrade to V4'
|
||||
|
||||
[forbidden."Nri.Ui.Switch.V1"]
|
||||
hint = 'upgrade to V2'
|
||||
|
||||
@ -197,6 +200,9 @@ hint = 'upgrade to V5'
|
||||
[forbidden."Nri.Ui.Table.V5"]
|
||||
hint = 'upgrade to V6'
|
||||
|
||||
[forbidden."Nri.Ui.Table.V6"]
|
||||
hint = 'upgrade to V7'
|
||||
|
||||
[forbidden."Nri.Ui.Tabs.V6"]
|
||||
hint = 'upgrade to V8'
|
||||
|
||||
|
@ -32,7 +32,7 @@ import Nri.Ui.Colors.V1
|
||||
import Nri.Ui.CssVendorPrefix.V1 as CssVendorPrefix
|
||||
import Nri.Ui.Fonts.V1 as Fonts
|
||||
import Nri.Ui.Svg.V1
|
||||
import Nri.Ui.Table.V6 as Table exposing (SortDirection(..))
|
||||
import Nri.Ui.Table.V7 as Table exposing (SortDirection(..))
|
||||
import Nri.Ui.UiIcon.V1
|
||||
|
||||
|
||||
@ -189,8 +189,7 @@ viewLoading config state =
|
||||
tableColumns =
|
||||
List.map (buildTableColumn config.updateMsg state) config.columns
|
||||
in
|
||||
Table.viewLoading
|
||||
tableColumns
|
||||
Table.viewLoading [] tableColumns
|
||||
|
||||
|
||||
{-| -}
|
||||
@ -203,7 +202,7 @@ view config state entries =
|
||||
sorter =
|
||||
findSorter config.columns state.column
|
||||
in
|
||||
Table.view
|
||||
Table.view []
|
||||
tableColumns
|
||||
(List.sortWith (sorter state.sortDirection) entries)
|
||||
|
||||
|
505
src/Nri/Ui/SortableTable/V4.elm
Normal file
505
src/Nri/Ui/SortableTable/V4.elm
Normal file
@ -0,0 +1,505 @@
|
||||
module Nri.Ui.SortableTable.V4 exposing
|
||||
( Column, Sorter, State
|
||||
, init, initDescending
|
||||
, custom, string
|
||||
, Attribute, updateMsg, state, stickyHeader, stickyHeaderCustom, StickyConfig, view, viewLoading
|
||||
, invariantSort, simpleSort, combineSorters
|
||||
)
|
||||
|
||||
{-| Changes from V3:
|
||||
|
||||
- Change to an HTML-like API
|
||||
- Allow the table header to be sticky
|
||||
|
||||
@docs Column, Sorter, State
|
||||
@docs init, initDescending
|
||||
@docs custom, string
|
||||
@docs Attribute, updateMsg, state, stickyHeader, stickyHeaderCustom, StickyConfig, view, viewLoading
|
||||
@docs invariantSort, simpleSort, combineSorters
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled.Aria as Aria
|
||||
import Css exposing (..)
|
||||
import Css.Global
|
||||
import Html.Styled as Html exposing (Html)
|
||||
import Html.Styled.Attributes exposing (css)
|
||||
import Html.Styled.Events
|
||||
import Nri.Ui.Colors.V1
|
||||
import Nri.Ui.CssVendorPrefix.V1 as CssVendorPrefix
|
||||
import Nri.Ui.Fonts.V1 as Fonts
|
||||
import Nri.Ui.Html.Attributes.V2 exposing (maybe)
|
||||
import Nri.Ui.Html.V3 exposing (viewJust)
|
||||
import Nri.Ui.Svg.V1
|
||||
import Nri.Ui.Table.V7 as Table exposing (SortDirection(..))
|
||||
import Nri.Ui.UiIcon.V1
|
||||
|
||||
|
||||
{-| -}
|
||||
type alias Sorter a =
|
||||
SortDirection -> a -> a -> Order
|
||||
|
||||
|
||||
{-| -}
|
||||
type Column id entry msg
|
||||
= Column
|
||||
{ id : id
|
||||
, header : Html msg
|
||||
, view : entry -> Html msg
|
||||
, sorter : Maybe (Sorter entry)
|
||||
, width : Int
|
||||
, cellStyles : entry -> List Style
|
||||
}
|
||||
|
||||
|
||||
{-| -}
|
||||
type alias State id =
|
||||
{ column : id
|
||||
, sortDirection : SortDirection
|
||||
}
|
||||
|
||||
|
||||
{-| -}
|
||||
type alias Config id msg =
|
||||
{ updateMsg : Maybe (State id -> msg)
|
||||
, state : Maybe (State id)
|
||||
, stickyHeader : Maybe StickyConfig
|
||||
}
|
||||
|
||||
|
||||
defaultConfig : Config id msg
|
||||
defaultConfig =
|
||||
{ updateMsg = Nothing
|
||||
, state = Nothing
|
||||
, stickyHeader = Nothing
|
||||
}
|
||||
|
||||
|
||||
{-| How the header will be set up to be sticky.
|
||||
|
||||
- `topOffset` controls how far off the top of the viewport the headers will
|
||||
stick, in pixels. (**Default value:** 0)
|
||||
- `zIndex` controls where in the stacking context the header will end
|
||||
up. Useful to prevent elements in rows from appearing over the header.
|
||||
(**Default value:** 0)
|
||||
|
||||
Headers are never sticky on mobile-sized viewports because doing so causes some
|
||||
accessibility issues with zooming and panning.
|
||||
|
||||
-}
|
||||
type alias StickyConfig =
|
||||
{ topOffset : Float
|
||||
, zIndex : Int
|
||||
, pageBackgroundColor : Css.Color
|
||||
}
|
||||
|
||||
|
||||
defaultStickyConfig : StickyConfig
|
||||
defaultStickyConfig =
|
||||
{ topOffset = 0
|
||||
, zIndex = 0
|
||||
, pageBackgroundColor = Nri.Ui.Colors.V1.white
|
||||
}
|
||||
|
||||
|
||||
stickyConfigStyles : StickyConfig -> List Style
|
||||
stickyConfigStyles { topOffset, zIndex, pageBackgroundColor } =
|
||||
[ Css.Global.children
|
||||
[ Css.Global.thead
|
||||
[ Css.position Css.sticky
|
||||
, Css.top (Css.px topOffset)
|
||||
, Css.zIndex (Css.int zIndex)
|
||||
, Css.backgroundColor pageBackgroundColor
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
{-| Customize how the table is rendered, for example by adding sorting or
|
||||
stickiness.
|
||||
-}
|
||||
type Attribute id msg
|
||||
= Attribute (Config id msg -> Config id msg)
|
||||
|
||||
|
||||
{-| Sort a column. You can get an initial state with `init` or `initDescending`.
|
||||
If you make this sorting interactive, you should store the state in your model
|
||||
and provide it to this function instead of recreating it on every update.
|
||||
-}
|
||||
state : State id -> Attribute id msg
|
||||
state state_ =
|
||||
Attribute (\config -> { config | state = Just state_ })
|
||||
|
||||
|
||||
{-| Add interactivity in sorting columns. When this attribute is provided and
|
||||
sorting is enabled, columns will be sortable by clicking the headers.
|
||||
-}
|
||||
updateMsg : (State id -> msg) -> Attribute id msg
|
||||
updateMsg updateMsg_ =
|
||||
Attribute (\config -> { config | updateMsg = Just updateMsg_ })
|
||||
|
||||
|
||||
{-| Make the header sticky (that is, it will stick to the top of the viewport
|
||||
when it otherwise would have been scrolled off.) You probably will want to set a
|
||||
background color on the header as well.
|
||||
-}
|
||||
stickyHeader : Attribute id msg
|
||||
stickyHeader =
|
||||
Attribute (\config -> { config | stickyHeader = Just defaultStickyConfig })
|
||||
|
||||
|
||||
{-| Does the same thing as `stickyHeader`, but with adaptations for your
|
||||
specific use.
|
||||
-}
|
||||
stickyHeaderCustom : StickyConfig -> Attribute id msg
|
||||
stickyHeaderCustom stickyConfig =
|
||||
Attribute (\config -> { config | stickyHeader = Just stickyConfig })
|
||||
|
||||
|
||||
{-| -}
|
||||
init : id -> State id
|
||||
init initialSort =
|
||||
{ column = initialSort
|
||||
, sortDirection = Ascending
|
||||
}
|
||||
|
||||
|
||||
{-| -}
|
||||
initDescending : id -> State id
|
||||
initDescending initialSort =
|
||||
{ column = initialSort
|
||||
, sortDirection = Descending
|
||||
}
|
||||
|
||||
|
||||
{-| -}
|
||||
string :
|
||||
{ id : id
|
||||
, header : String
|
||||
, value : entry -> String
|
||||
, width : Int
|
||||
, cellStyles : entry -> List Style
|
||||
}
|
||||
-> Column id entry msg
|
||||
string { id, header, value, width, cellStyles } =
|
||||
Column
|
||||
{ id = id
|
||||
, header = Html.text header
|
||||
, view = value >> Html.text
|
||||
, sorter = Just (simpleSort value)
|
||||
, width = width
|
||||
, cellStyles = cellStyles
|
||||
}
|
||||
|
||||
|
||||
{-| -}
|
||||
custom :
|
||||
{ id : id
|
||||
, header : Html msg
|
||||
, view : entry -> Html msg
|
||||
, sorter : Maybe (Sorter entry)
|
||||
, width : Int
|
||||
, cellStyles : entry -> List Style
|
||||
}
|
||||
-> Column id entry msg
|
||||
custom config =
|
||||
Column
|
||||
{ id = config.id
|
||||
, header = config.header
|
||||
, view = config.view
|
||||
, sorter = config.sorter
|
||||
, width = config.width
|
||||
, cellStyles = config.cellStyles
|
||||
}
|
||||
|
||||
|
||||
{-| Create a sorter function that always orders the entries in the same order.
|
||||
For example, this is useful when we want to resolve ties and sort the tied
|
||||
entries by name, no matter of the sort direction set on the table.
|
||||
-}
|
||||
invariantSort : (entry -> comparable) -> Sorter entry
|
||||
invariantSort mapper =
|
||||
\_ elem1 elem2 ->
|
||||
compare (mapper elem1) (mapper elem2)
|
||||
|
||||
|
||||
{-| Create a simple sorter function that orders entries by mapping a function
|
||||
over the collection. It will also reverse it when the sort direction is descending.
|
||||
-}
|
||||
simpleSort : (entry -> comparable) -> Sorter entry
|
||||
simpleSort mapper =
|
||||
\sortDirection elem1 elem2 ->
|
||||
let
|
||||
result =
|
||||
compare (mapper elem1) (mapper elem2)
|
||||
in
|
||||
case sortDirection of
|
||||
Ascending ->
|
||||
result
|
||||
|
||||
Descending ->
|
||||
flipOrder result
|
||||
|
||||
|
||||
flipOrder : Order -> Order
|
||||
flipOrder order =
|
||||
case order of
|
||||
LT ->
|
||||
GT
|
||||
|
||||
EQ ->
|
||||
EQ
|
||||
|
||||
GT ->
|
||||
LT
|
||||
|
||||
|
||||
{-| -}
|
||||
combineSorters : List (Sorter entry) -> Sorter entry
|
||||
combineSorters sorters =
|
||||
\sortDirection elem1 elem2 ->
|
||||
let
|
||||
folder =
|
||||
\sorter acc ->
|
||||
case acc of
|
||||
EQ ->
|
||||
sorter sortDirection elem1 elem2
|
||||
|
||||
_ ->
|
||||
acc
|
||||
in
|
||||
List.foldl folder EQ sorters
|
||||
|
||||
|
||||
{-| -}
|
||||
view : List (Attribute id msg) -> List (Column id entry msg) -> List entry -> Html msg
|
||||
view attributes columns entries =
|
||||
let
|
||||
config =
|
||||
List.foldl (\(Attribute fn) soFar -> fn soFar) defaultConfig attributes
|
||||
|
||||
stickyStyles =
|
||||
Maybe.map stickyConfigStyles config.stickyHeader
|
||||
|> Maybe.withDefault []
|
||||
|
||||
tableColumns =
|
||||
List.map (buildTableColumn config.updateMsg config.state) columns
|
||||
in
|
||||
case config.state of
|
||||
Just state_ ->
|
||||
let
|
||||
sorter =
|
||||
findSorter columns state_.column
|
||||
in
|
||||
Table.view stickyStyles
|
||||
tableColumns
|
||||
(List.sortWith (sorter state_.sortDirection) entries)
|
||||
|
||||
Nothing ->
|
||||
Table.view stickyStyles tableColumns entries
|
||||
|
||||
|
||||
{-| -}
|
||||
viewLoading : List (Attribute id msg) -> List (Column id entry msg) -> Html msg
|
||||
viewLoading attributes columns =
|
||||
let
|
||||
config =
|
||||
List.foldl (\(Attribute fn) soFar -> fn soFar) defaultConfig attributes
|
||||
|
||||
stickyStyles =
|
||||
Maybe.map stickyConfigStyles config.stickyHeader
|
||||
|> Maybe.withDefault []
|
||||
|
||||
tableColumns =
|
||||
List.map (buildTableColumn config.updateMsg config.state) columns
|
||||
in
|
||||
Table.viewLoading stickyStyles tableColumns
|
||||
|
||||
|
||||
findSorter : List (Column id entry msg) -> id -> Sorter entry
|
||||
findSorter columns columnId =
|
||||
columns
|
||||
|> listExtraFind (\(Column column) -> column.id == columnId)
|
||||
|> Maybe.andThen (\(Column column) -> column.sorter)
|
||||
|> Maybe.withDefault identitySorter
|
||||
|
||||
|
||||
{-| Taken from <https://github.com/elm-community/list-extra/blob/8.2.0/src/List/Extra.elm#L556>
|
||||
-}
|
||||
listExtraFind : (a -> Bool) -> List a -> Maybe a
|
||||
listExtraFind predicate list =
|
||||
case list of
|
||||
[] ->
|
||||
Nothing
|
||||
|
||||
first :: rest ->
|
||||
if predicate first then
|
||||
Just first
|
||||
|
||||
else
|
||||
listExtraFind predicate rest
|
||||
|
||||
|
||||
identitySorter : Sorter a
|
||||
identitySorter =
|
||||
\_ _ _ ->
|
||||
EQ
|
||||
|
||||
|
||||
buildTableColumn : Maybe (State id -> msg) -> Maybe (State id) -> Column id entry msg -> Table.Column entry msg
|
||||
buildTableColumn maybeUpdateMsg maybeState (Column column) =
|
||||
Table.custom
|
||||
{ header =
|
||||
case maybeState of
|
||||
Just state_ ->
|
||||
viewSortHeader (column.sorter /= Nothing) column.header maybeUpdateMsg state_ column.id
|
||||
|
||||
Nothing ->
|
||||
Debug.todo "non-sorted header"
|
||||
, view = column.view
|
||||
, width = Css.px (toFloat column.width)
|
||||
, cellStyles = column.cellStyles
|
||||
, sort =
|
||||
Maybe.andThen
|
||||
(\state_ ->
|
||||
if state_.column == column.id then
|
||||
Just state_.sortDirection
|
||||
|
||||
else
|
||||
Nothing
|
||||
)
|
||||
maybeState
|
||||
}
|
||||
|
||||
|
||||
viewSortHeader : Bool -> Html msg -> Maybe (State id -> msg) -> State id -> id -> Html msg
|
||||
viewSortHeader isSortable header maybeUpdateMsg state_ id =
|
||||
let
|
||||
nextState =
|
||||
nextTableState state_ id
|
||||
in
|
||||
if isSortable then
|
||||
Html.button
|
||||
[ css
|
||||
[ Css.displayFlex
|
||||
, Css.alignItems Css.center
|
||||
, Css.justifyContent Css.spaceBetween
|
||||
, CssVendorPrefix.property "user-select" "none"
|
||||
, if state_.column == id then
|
||||
fontWeight bold
|
||||
|
||||
else
|
||||
fontWeight normal
|
||||
, cursor pointer
|
||||
|
||||
-- make this look less "buttony"
|
||||
, Css.border Css.zero
|
||||
, Css.backgroundColor Css.transparent
|
||||
, Css.width (Css.pct 100)
|
||||
, Css.height (Css.pct 100)
|
||||
, Css.margin Css.zero
|
||||
, Css.padding Css.zero
|
||||
, Fonts.baseFont
|
||||
, Css.fontSize (Css.em 1)
|
||||
]
|
||||
, maybe (\updateMsg_ -> Html.Styled.Events.onClick (updateMsg_ nextState)) maybeUpdateMsg
|
||||
|
||||
-- screen readers should know what clicking this button will do
|
||||
, Aria.roleDescription "sort button"
|
||||
]
|
||||
[ Html.div [] [ header ]
|
||||
, viewJust (\_ -> viewSortButton state_ id) maybeUpdateMsg
|
||||
]
|
||||
|
||||
else
|
||||
Html.div
|
||||
[ css [ fontWeight normal ]
|
||||
]
|
||||
[ header ]
|
||||
|
||||
|
||||
viewSortButton : State id -> id -> Html msg
|
||||
viewSortButton state_ id =
|
||||
let
|
||||
arrows upHighlighted downHighlighted =
|
||||
Html.div
|
||||
[ css
|
||||
[ Css.displayFlex
|
||||
, Css.flexDirection Css.column
|
||||
, Css.alignItems Css.center
|
||||
, Css.justifyContent Css.center
|
||||
]
|
||||
]
|
||||
[ sortArrow Up upHighlighted
|
||||
, sortArrow Down downHighlighted
|
||||
]
|
||||
|
||||
buttonContent =
|
||||
case ( state_.column == id, state_.sortDirection ) of
|
||||
( True, Ascending ) ->
|
||||
arrows True False
|
||||
|
||||
( True, Descending ) ->
|
||||
arrows False True
|
||||
|
||||
( False, _ ) ->
|
||||
arrows False False
|
||||
in
|
||||
Html.div [ css [ padding (px 2) ] ] [ buttonContent ]
|
||||
|
||||
|
||||
nextTableState : State id -> id -> State id
|
||||
nextTableState state_ id =
|
||||
if state_.column == id then
|
||||
{ column = id
|
||||
, sortDirection = flipSortDirection state_.sortDirection
|
||||
}
|
||||
|
||||
else
|
||||
{ column = id
|
||||
, sortDirection = Ascending
|
||||
}
|
||||
|
||||
|
||||
flipSortDirection : SortDirection -> SortDirection
|
||||
flipSortDirection order =
|
||||
case order of
|
||||
Ascending ->
|
||||
Descending
|
||||
|
||||
Descending ->
|
||||
Ascending
|
||||
|
||||
|
||||
type Direction
|
||||
= Up
|
||||
| Down
|
||||
|
||||
|
||||
sortArrow : Direction -> Bool -> Html msg
|
||||
sortArrow direction active =
|
||||
let
|
||||
arrow =
|
||||
case direction of
|
||||
Up ->
|
||||
Nri.Ui.UiIcon.V1.sortArrow
|
||||
|
||||
Down ->
|
||||
Nri.Ui.UiIcon.V1.sortArrowDown
|
||||
|
||||
color =
|
||||
if active then
|
||||
Nri.Ui.Colors.V1.azure
|
||||
|
||||
else
|
||||
Nri.Ui.Colors.V1.gray75
|
||||
in
|
||||
arrow
|
||||
|> Nri.Ui.Svg.V1.withHeight (px 6)
|
||||
|> Nri.Ui.Svg.V1.withWidth (px 8)
|
||||
|> Nri.Ui.Svg.V1.withColor color
|
||||
|> Nri.Ui.Svg.V1.withCss
|
||||
[ displayFlex
|
||||
, margin2 (px 1) zero
|
||||
]
|
||||
|> Nri.Ui.Svg.V1.toHtml
|
336
src/Nri/Ui/Table/V7.elm
Normal file
336
src/Nri/Ui/Table/V7.elm
Normal file
@ -0,0 +1,336 @@
|
||||
module Nri.Ui.Table.V7 exposing
|
||||
( Column, SortDirection(..), custom, string, rowHeader
|
||||
, view, viewWithoutHeader
|
||||
, viewLoading, viewLoadingWithoutHeader
|
||||
)
|
||||
|
||||
{-| Upgrading from V6:
|
||||
|
||||
- If you don't have extra styles for the table, add an empty list to calls
|
||||
to `view`.
|
||||
- Consider moving to `SortableTable`, as in V4 a non-interactive table renders
|
||||
just the same but has additional flexibility in the API and will make future
|
||||
upgrades easier.
|
||||
|
||||
@docs Column, SortDirection, custom, string, rowHeader
|
||||
|
||||
@docs view, viewWithoutHeader
|
||||
|
||||
@docs viewLoading, viewLoadingWithoutHeader
|
||||
|
||||
-}
|
||||
|
||||
import Accessibility.Styled.Style as Style
|
||||
import Css exposing (..)
|
||||
import Css.Animations
|
||||
import Html.Styled as Html exposing (..)
|
||||
import Html.Styled.Attributes as Attributes exposing (css)
|
||||
import Nri.Ui.Colors.V1 exposing (..)
|
||||
import Nri.Ui.Fonts.V1 exposing (baseFont)
|
||||
|
||||
|
||||
{-| Closed representation of how to render the header and cells of a column
|
||||
in the table
|
||||
-}
|
||||
type Column data msg
|
||||
= Column (Html msg) (data -> Html msg) Style (data -> List Style) (Maybe SortDirection) CellType
|
||||
|
||||
|
||||
{-| Which direction is a table column sorted? Only set these on columns that
|
||||
actually have an explicit sort!
|
||||
-}
|
||||
type SortDirection
|
||||
= Ascending
|
||||
| Descending
|
||||
|
||||
|
||||
{-| Is this cell a data cell or header cell?
|
||||
-}
|
||||
type CellType
|
||||
= RowHeaderCell
|
||||
| DataCell
|
||||
|
||||
|
||||
cell : CellType -> List (Attribute msg) -> List (Html msg) -> Html msg
|
||||
cell cellType attrs =
|
||||
case cellType of
|
||||
RowHeaderCell ->
|
||||
th (Attributes.scope "row" :: attrs)
|
||||
|
||||
DataCell ->
|
||||
td attrs
|
||||
|
||||
|
||||
{-| A column that renders some aspect of a value as text
|
||||
-}
|
||||
string :
|
||||
{ header : String
|
||||
, value : data -> String
|
||||
, width : LengthOrAuto compatible
|
||||
, cellStyles : data -> List Style
|
||||
, sort : Maybe SortDirection
|
||||
}
|
||||
-> Column data msg
|
||||
string { header, value, width, cellStyles, sort } =
|
||||
Column (Html.text header) (value >> Html.text) (Css.width width) cellStyles sort DataCell
|
||||
|
||||
|
||||
{-| A column that renders however you want it to
|
||||
-}
|
||||
custom :
|
||||
{ header : Html msg
|
||||
, view : data -> Html msg
|
||||
, width : LengthOrAuto compatible
|
||||
, cellStyles : data -> List Style
|
||||
, sort : Maybe SortDirection
|
||||
}
|
||||
-> Column data msg
|
||||
custom options =
|
||||
Column options.header options.view (Css.width options.width) options.cellStyles options.sort DataCell
|
||||
|
||||
|
||||
{-| A column whose cells are row headers
|
||||
-}
|
||||
rowHeader :
|
||||
{ header : Html msg
|
||||
, view : data -> Html msg
|
||||
, width : LengthOrAuto compatible
|
||||
, cellStyles : data -> List Style
|
||||
, sort : Maybe SortDirection
|
||||
}
|
||||
-> Column data msg
|
||||
rowHeader options =
|
||||
Column options.header options.view (Css.width options.width) options.cellStyles options.sort RowHeaderCell
|
||||
|
||||
|
||||
|
||||
-- VIEW
|
||||
|
||||
|
||||
{-| Displays a table of data without a header row
|
||||
-}
|
||||
viewWithoutHeader : List Style -> List (Column data msg) -> List data -> Html msg
|
||||
viewWithoutHeader additionalStyles columns =
|
||||
tableWithoutHeader additionalStyles columns (viewRow columns)
|
||||
|
||||
|
||||
{-| Displays a table of data based on the provided column definitions
|
||||
-}
|
||||
view : List Style -> List (Column data msg) -> List data -> Html msg
|
||||
view additionalStyles columns =
|
||||
tableWithHeader additionalStyles columns (viewRow columns)
|
||||
|
||||
|
||||
viewRow : List (Column data msg) -> data -> Html msg
|
||||
viewRow columns data =
|
||||
tr
|
||||
[ css rowStyles ]
|
||||
(List.map (viewColumn data) columns)
|
||||
|
||||
|
||||
viewColumn : data -> Column data msg -> Html msg
|
||||
viewColumn data (Column _ renderer width cellStyles _ cellType) =
|
||||
cell cellType
|
||||
[ css ([ width, verticalAlign middle ] ++ cellStyles data)
|
||||
]
|
||||
[ renderer data ]
|
||||
|
||||
|
||||
|
||||
-- VIEW LOADING
|
||||
|
||||
|
||||
{-| Display a table with the given columns but instead of data, show blocked
|
||||
out text with an interesting animation. This view lets the user know that
|
||||
data is on its way and what it will look like when it arrives.
|
||||
-}
|
||||
viewLoading : List Style -> List (Column data msg) -> Html msg
|
||||
viewLoading additionalStyles columns =
|
||||
tableWithHeader (loadingTableStyles ++ additionalStyles) columns (viewLoadingRow columns) (List.range 0 8)
|
||||
|
||||
|
||||
{-| Display the loading table without a header row
|
||||
-}
|
||||
viewLoadingWithoutHeader : List Style -> List (Column data msg) -> Html msg
|
||||
viewLoadingWithoutHeader additionalStyles columns =
|
||||
tableWithoutHeader (loadingTableStyles ++ additionalStyles) columns (viewLoadingRow columns) (List.range 0 8)
|
||||
|
||||
|
||||
viewLoadingRow : List (Column data msg) -> Int -> Html msg
|
||||
viewLoadingRow columns index =
|
||||
tr
|
||||
[ css rowStyles ]
|
||||
(List.indexedMap (viewLoadingColumn index) columns)
|
||||
|
||||
|
||||
viewLoadingColumn : Int -> Int -> Column data msg -> Html msg
|
||||
viewLoadingColumn rowIndex colIndex (Column _ _ width _ _ cellType) =
|
||||
cell cellType
|
||||
[ css (stylesLoadingColumn rowIndex colIndex width ++ [ verticalAlign middle ] ++ loadingCellStyles)
|
||||
]
|
||||
[ span [ css loadingContentStyles ] [] ]
|
||||
|
||||
|
||||
stylesLoadingColumn : Int -> Int -> Style -> List Style
|
||||
stylesLoadingColumn rowIndex colIndex width =
|
||||
[ width
|
||||
, property "animation-delay" (String.fromFloat (toFloat (rowIndex + colIndex) * 0.1) ++ "s")
|
||||
]
|
||||
|
||||
|
||||
|
||||
-- HELP
|
||||
|
||||
|
||||
tableWithoutHeader : List Style -> List (Column data msg) -> (a -> Html msg) -> List a -> Html msg
|
||||
tableWithoutHeader styles columns toRow data =
|
||||
table styles
|
||||
[ thead [] [ tr Style.invisible (List.map tableColHeader columns) ]
|
||||
, tableBody toRow data
|
||||
]
|
||||
|
||||
|
||||
tableWithHeader : List Style -> List (Column data msg) -> (a -> Html msg) -> List a -> Html msg
|
||||
tableWithHeader styles columns toRow data =
|
||||
table styles
|
||||
[ tableHeader columns
|
||||
, tableBody toRow data
|
||||
]
|
||||
|
||||
|
||||
table : List Style -> List (Html msg) -> Html msg
|
||||
table styles =
|
||||
Html.table [ css (styles ++ tableStyles) ]
|
||||
|
||||
|
||||
tableHeader : List (Column data msg) -> Html msg
|
||||
tableHeader columns =
|
||||
thead []
|
||||
[ tr [ css headersStyles ]
|
||||
(List.map tableColHeader columns)
|
||||
]
|
||||
|
||||
|
||||
tableColHeader : Column data msg -> Html msg
|
||||
tableColHeader (Column header _ width _ sort _) =
|
||||
th
|
||||
[ Attributes.scope "col"
|
||||
, css (width :: headerStyles)
|
||||
, Attributes.attribute "aria-sort" <|
|
||||
case sort of
|
||||
Nothing ->
|
||||
"none"
|
||||
|
||||
Just Ascending ->
|
||||
"ascending"
|
||||
|
||||
Just Descending ->
|
||||
"descending"
|
||||
]
|
||||
[ header ]
|
||||
|
||||
|
||||
tableBody : (a -> Html msg) -> List a -> Html msg
|
||||
tableBody toRow items =
|
||||
tbody [] (List.map toRow items)
|
||||
|
||||
|
||||
|
||||
-- STYLES
|
||||
|
||||
|
||||
headersStyles : List Style
|
||||
headersStyles =
|
||||
[ -- We use a inset box shadown for a bottom border instead of an actual
|
||||
-- border because with our use of `border-collapse: collapse`, the bottom
|
||||
-- gray border sticks to the table instead of traveling with the header
|
||||
-- when the header has `position: sticky` applied.
|
||||
boxShadow4 inset (px 0) (px -3) gray75
|
||||
, height (px 45)
|
||||
, fontSize (px 15)
|
||||
]
|
||||
|
||||
|
||||
headerStyles : List Style
|
||||
headerStyles =
|
||||
[ padding4 (px 11) (px 12) (px 14) (px 12)
|
||||
, textAlign left
|
||||
, fontWeight bold
|
||||
]
|
||||
|
||||
|
||||
rowStyles : List Style
|
||||
rowStyles =
|
||||
[ height (px 45)
|
||||
, fontSize (px 14)
|
||||
, color gray20
|
||||
, pseudoClass "nth-child(odd)"
|
||||
[ backgroundColor gray96 ]
|
||||
]
|
||||
|
||||
|
||||
loadingContentStyles : List Style
|
||||
loadingContentStyles =
|
||||
[ width (pct 100)
|
||||
, display inlineBlock
|
||||
, height (Css.em 1)
|
||||
, borderRadius (Css.em 1)
|
||||
, backgroundColor gray75
|
||||
]
|
||||
|
||||
|
||||
loadingCellStyles : List Style
|
||||
loadingCellStyles =
|
||||
[ batch flashAnimation
|
||||
, padding2 (px 14) (px 10)
|
||||
]
|
||||
|
||||
|
||||
loadingTableStyles : List Style
|
||||
loadingTableStyles =
|
||||
fadeInAnimation
|
||||
|
||||
|
||||
tableStyles : List Style
|
||||
tableStyles =
|
||||
[ borderCollapse collapse
|
||||
, baseFont
|
||||
, Css.width (Css.pct 100)
|
||||
]
|
||||
|
||||
|
||||
flash : Css.Animations.Keyframes {}
|
||||
flash =
|
||||
Css.Animations.keyframes
|
||||
[ ( 0, [ Css.Animations.opacity (Css.num 0.6) ] )
|
||||
, ( 50, [ Css.Animations.opacity (Css.num 0.2) ] )
|
||||
, ( 100, [ Css.Animations.opacity (Css.num 0.6) ] )
|
||||
]
|
||||
|
||||
|
||||
fadeIn : Css.Animations.Keyframes {}
|
||||
fadeIn =
|
||||
Css.Animations.keyframes
|
||||
[ ( 0, [ Css.Animations.opacity (Css.num 0) ] )
|
||||
, ( 100, [ Css.Animations.opacity (Css.num 1) ] )
|
||||
]
|
||||
|
||||
|
||||
flashAnimation : List Css.Style
|
||||
flashAnimation =
|
||||
[ animationName flash
|
||||
, property "animation-duration" "2s"
|
||||
, property "animation-iteration-count" "infinite"
|
||||
, opacity (num 0.6)
|
||||
]
|
||||
|
||||
|
||||
fadeInAnimation : List Css.Style
|
||||
fadeInAnimation =
|
||||
[ animationName fadeIn
|
||||
, property "animation-duration" "0.4s"
|
||||
, property "animation-delay" "0.2s"
|
||||
, property "animation-fill-mode" "forwards"
|
||||
, animationIterationCount (int 1)
|
||||
, opacity (num 0)
|
||||
]
|
@ -2,7 +2,8 @@ module Spec.Nri.Ui.SortableTable exposing (spec)
|
||||
|
||||
import Expect
|
||||
import Html.Styled
|
||||
import Nri.Ui.SortableTable.V3 as SortableTable
|
||||
import Nri.Ui.SortableTable.V4 as SortableTable
|
||||
import Nri.Ui.Table.V7 exposing (SortDirection)
|
||||
import Test exposing (..)
|
||||
import Test.Html.Query as Query
|
||||
import Test.Html.Selector as Selector
|
||||
@ -19,30 +20,23 @@ type alias Person =
|
||||
}
|
||||
|
||||
|
||||
type Msg
|
||||
= NoOp
|
||||
|
||||
|
||||
config : SortableTable.Config Column Person Msg
|
||||
config =
|
||||
{ updateMsg = \_ -> NoOp
|
||||
, columns =
|
||||
[ SortableTable.string
|
||||
{ id = FirstName
|
||||
, header = "First name"
|
||||
, value = .firstName
|
||||
, width = 125
|
||||
, cellStyles = \_ -> []
|
||||
}
|
||||
, SortableTable.string
|
||||
{ id = LastName
|
||||
, header = "Last name"
|
||||
, value = .lastName
|
||||
, width = 125
|
||||
, cellStyles = \_ -> []
|
||||
}
|
||||
]
|
||||
}
|
||||
columns : List (SortableTable.Column Column Person msg)
|
||||
columns =
|
||||
[ SortableTable.string
|
||||
{ id = FirstName
|
||||
, header = "First name"
|
||||
, value = .firstName
|
||||
, width = 125
|
||||
, cellStyles = \_ -> []
|
||||
}
|
||||
, SortableTable.string
|
||||
{ id = LastName
|
||||
, header = "Last name"
|
||||
, value = .lastName
|
||||
, width = 125
|
||||
, cellStyles = \_ -> []
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
entries : List Person
|
||||
@ -53,9 +47,9 @@ entries =
|
||||
]
|
||||
|
||||
|
||||
tableView : SortableTable.State Column -> Query.Single Msg
|
||||
tableView : SortableTable.State Column -> Query.Single msg
|
||||
tableView sortState =
|
||||
SortableTable.view config sortState entries
|
||||
SortableTable.view [ SortableTable.state sortState ] columns entries
|
||||
|> Html.Styled.toUnstyled
|
||||
|> Query.fromHtml
|
||||
|
||||
@ -70,10 +64,12 @@ sortByDescending field =
|
||||
SortableTable.initDescending field
|
||||
|
||||
|
||||
ascending : SortDirection
|
||||
ascending =
|
||||
sortBy FirstName |> .sortDirection
|
||||
|
||||
|
||||
descending : SortDirection
|
||||
descending =
|
||||
sortByDescending FirstName |> .sortDirection
|
||||
|
||||
|
@ -66,11 +66,13 @@
|
||||
"Nri.Ui.Shadows.V1",
|
||||
"Nri.Ui.SideNav.V4",
|
||||
"Nri.Ui.SortableTable.V3",
|
||||
"Nri.Ui.SortableTable.V4",
|
||||
"Nri.Ui.Spacing.V1",
|
||||
"Nri.Ui.Sprite.V1",
|
||||
"Nri.Ui.Svg.V1",
|
||||
"Nri.Ui.Switch.V2",
|
||||
"Nri.Ui.Table.V6",
|
||||
"Nri.Ui.Table.V7",
|
||||
"Nri.Ui.Tabs.V6",
|
||||
"Nri.Ui.Tabs.V7",
|
||||
"Nri.Ui.Tabs.V8",
|
||||
|
Loading…
Reference in New Issue
Block a user