Upgraded table versions to allow for additional cell styles

This commit is contained in:
Charlie Koster 2019-08-05 12:49:39 -05:00
parent 4de053fcd1
commit ae5e33f919
5 changed files with 680 additions and 23 deletions

View File

@ -67,8 +67,10 @@
"Nri.Ui.SlideModal.V1",
"Nri.Ui.SlideModal.V2",
"Nri.Ui.SortableTable.V1",
"Nri.Ui.SortableTable.V2",
"Nri.Ui.Table.V3",
"Nri.Ui.Table.V4",
"Nri.Ui.Table.V5",
"Nri.Ui.Tabs.V3",
"Nri.Ui.Tabs.V4",
"Nri.Ui.Text.V2",

View File

@ -1,4 +1,4 @@
module Nri.Ui.SortableTable.V1 exposing
module Nri.Ui.SortableTable.V2 exposing
( Column, Config, Sorter, State
, init, initDescending
, custom, string, view, viewLoading
@ -23,7 +23,7 @@ import Html.Styled.Events
import Nri.Ui.Colors.Extra
import Nri.Ui.Colors.V1
import Nri.Ui.CssVendorPrefix.V1 as CssVendorPrefix
import Nri.Ui.Table.V4
import Nri.Ui.Table.V5
import Svg.Styled as Svg exposing (Svg)
import Svg.Styled.Attributes as SvgAttributes
@ -46,6 +46,7 @@ type Column id entry msg
, view : entry -> Html msg
, sorter : Sorter entry
, width : Int
, cellStyles : entry -> List Style
}
@ -85,15 +86,17 @@ string :
, header : String
, value : entry -> String
, width : Int
, cellStyles : entry -> List Style
}
-> Column id entry msg
string { id, header, value, width } =
string { id, header, value, width, cellStyles } =
Column
{ id = id
, header = Html.text header
, view = value >> Html.text
, sorter = simpleSort value
, width = width
, cellStyles = cellStyles
}
@ -104,6 +107,7 @@ custom :
, view : entry -> Html msg
, sorter : Sorter entry
, width : Int
, cellStyles : entry -> List Style
}
-> Column id entry msg
custom config =
@ -113,6 +117,7 @@ custom config =
, view = config.view
, sorter = config.sorter
, width = config.width
, cellStyles = config.cellStyles
}
@ -181,7 +186,7 @@ viewLoading config state =
tableColumns =
List.map (buildTableColumn config.updateMsg state) config.columns
in
Nri.Ui.Table.V4.viewLoading
Nri.Ui.Table.V5.viewLoading
tableColumns
@ -195,7 +200,7 @@ view config state entries =
sorter =
findSorter config.columns state.column
in
Nri.Ui.Table.V4.view
Nri.Ui.Table.V5.view
tableColumns
(List.sortWith (sorter state.sortDirection) entries)
@ -230,12 +235,13 @@ identitySorter =
EQ
buildTableColumn : (State id -> msg) -> State id -> Column id entry msg -> Nri.Ui.Table.V4.Column entry msg
buildTableColumn : (State id -> msg) -> State id -> Column id entry msg -> Nri.Ui.Table.V5.Column entry msg
buildTableColumn updateMsg state (Column column) =
Nri.Ui.Table.V4.custom
Nri.Ui.Table.V5.custom
{ header = viewSortHeader column.header updateMsg state column.id
, view = column.view
, width = Css.px (toFloat column.width)
, cellStyles = column.cellStyles
}

View File

@ -0,0 +1,368 @@
module Nri.Ui.SortableTable.V2 exposing
( Column, Config, Sorter, State
, init, initDescending
, custom, string, view, viewLoading
, invariantSort, simpleSort, combineSorters
)
{-|
@docs Column, Config, Sorter, State
@docs init, initDescending
@docs custom, string, view, viewLoading
@docs invariantSort, simpleSort, combineSorters
-}
import Color
import Css exposing (..)
import Css.Global exposing (Snippet, adjacentSiblings, children, class, descendants, each, everything, media, selector, withClass)
import Html.Styled as Html exposing (Html)
import Html.Styled.Attributes exposing (css)
import Html.Styled.Events
import Nri.Ui.Colors.Extra
import Nri.Ui.Colors.V1
import Nri.Ui.CssVendorPrefix.V1 as CssVendorPrefix
import Nri.Ui.Table.V5
import Svg.Styled as Svg exposing (Svg)
import Svg.Styled.Attributes as SvgAttributes
type SortDirection
= Ascending
| Descending
{-| -}
type alias Sorter a =
SortDirection -> a -> a -> Order
{-| -}
type Column id entry msg
= Column
{ id : id
, header : Html msg
, view : entry -> Html msg
, sorter : Sorter entry
, width : Int
, cellStyles : entry -> List Style
}
{-| -}
type alias State id =
{ column : id
, sortDirection : SortDirection
}
{-| -}
type alias Config id entry msg =
{ updateMsg : State id -> msg
, columns : List (Column id entry msg)
}
{-| -}
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 = simpleSort value
, width = width
, cellStyles = cellStyles
}
{-| -}
custom :
{ id : id
, header : Html msg
, view : entry -> Html msg
, sorter : 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 =
\sortDirection 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
{-| -}
viewLoading : Config id entry msg -> State id -> Html msg
viewLoading config state =
let
tableColumns =
List.map (buildTableColumn config.updateMsg state) config.columns
in
Nri.Ui.Table.V5.viewLoading
tableColumns
{-| -}
view : Config id entry msg -> State id -> List entry -> Html msg
view config state entries =
let
tableColumns =
List.map (buildTableColumn config.updateMsg state) config.columns
sorter =
findSorter config.columns state.column
in
Nri.Ui.Table.V5.view
tableColumns
(List.sortWith (sorter state.sortDirection) entries)
findSorter : List (Column id entry msg) -> id -> Sorter entry
findSorter columns columnId =
columns
|> listExtraFind (\(Column column) -> column.id == columnId)
|> Maybe.map (\(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 =
\sortDirection item1 item2 ->
EQ
buildTableColumn : (State id -> msg) -> State id -> Column id entry msg -> Nri.Ui.Table.V5.Column entry msg
buildTableColumn updateMsg state (Column column) =
Nri.Ui.Table.V5.custom
{ header = viewSortHeader column.header updateMsg state column.id
, view = column.view
, width = Css.px (toFloat column.width)
, cellStyles = column.cellStyles
}
viewSortHeader : Html msg -> (State id -> msg) -> State id -> id -> Html msg
viewSortHeader header updateMsg state id =
let
nextState =
nextTableState state id
in
Html.div
[ css
[ Css.displayFlex
, Css.alignItems Css.center
, Css.justifyContent Css.spaceBetween
, cursor pointer
, CssVendorPrefix.property "user-select" "none"
, if state.column == id then
fontWeight bold
else
fontWeight normal
]
, Html.Styled.Events.onClick (updateMsg nextState)
]
[ Html.div [] [ header ]
, viewSortButton updateMsg state id
]
viewSortButton : (State id -> msg) -> State id -> id -> Html msg
viewSortButton updateMsg 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 =
Html.div
[ css
[ width (px 8)
, height (px 6)
, position relative
, margin2 (px 1) zero
]
]
[ Svg.svg
[ SvgAttributes.viewBox "0 0 8 6"
, SvgAttributes.css
[ position absolute
, top zero
, left zero
, case direction of
Up ->
Css.batch []
Down ->
Css.batch [ transform <| rotate (deg 180) ]
]
, if active then
SvgAttributes.fill (toCssString Nri.Ui.Colors.V1.azure)
else
SvgAttributes.fill (toCssString Nri.Ui.Colors.V1.gray75)
]
[ Svg.polygon [ SvgAttributes.points "0 6 4 0 8 6 0 6" ] []
]
]
toCssString : Css.Color -> String
toCssString =
Color.toCssString << Nri.Ui.Colors.Extra.toCoreColor

View File

@ -1,4 +1,4 @@
module Nri.Ui.Table.V4 exposing
module Nri.Ui.Table.V5 exposing
( Column, custom, string
, view, viewWithoutHeader
, viewLoading, viewLoadingWithoutHeader
@ -39,7 +39,7 @@ import Nri.Ui.Fonts.V1 exposing (baseFont)
in the table
-}
type Column data msg
= Column (Html msg) (data -> Html msg) Style
= Column (Html msg) (data -> Html msg) Style (data -> List Style)
{-| A column that renders some aspect of a value as text
@ -48,10 +48,11 @@ string :
{ header : String
, value : data -> String
, width : LengthOrAuto compatible
, cellStyles : data -> List Style
}
-> Column data msg
string { header, value, width } =
Column (Html.text header) (value >> Html.text) (Css.width width)
string { header, value, width, cellStyles } =
Column (Html.text header) (value >> Html.text) (Css.width width) cellStyles
{-| A column that renders however you want it to
@ -60,10 +61,11 @@ custom :
{ header : Html msg
, view : data -> Html msg
, width : LengthOrAuto compatible
, cellStyles : data -> List Style
}
-> Column data msg
custom options =
Column options.header options.view (Css.width options.width)
Column options.header options.view (Css.width options.width) options.cellStyles
@ -92,9 +94,9 @@ viewRow columns data =
viewColumn : data -> Column data msg -> Html msg
viewColumn data (Column _ renderer width) =
viewColumn data (Column _ renderer width cellStyles) =
td
[ css (width :: cellStyles)
[ css ([ width, verticalAlign middle ] ++ cellStyles data)
]
[ renderer data ]
@ -127,9 +129,9 @@ viewLoadingRow columns index =
viewLoadingColumn : Int -> Int -> Column data msg -> Html msg
viewLoadingColumn rowIndex colIndex (Column _ _ width) =
viewLoadingColumn rowIndex colIndex (Column _ _ width _) =
td
[ css (stylesLoadingColumn rowIndex colIndex width ++ cellStyles ++ loadingCellStyles)
[ css (stylesLoadingColumn rowIndex colIndex width ++ [ verticalAlign middle ] ++ loadingCellStyles)
]
[ span [ css loadingContentStyles ] [] ]
@ -174,7 +176,7 @@ tableHeader columns =
tableRowHeader : Column data msg -> Html msg
tableRowHeader (Column header _ width) =
tableRowHeader (Column header _ width _) =
th
[ css (width :: headerStyles)
]
@ -216,12 +218,6 @@ rowStyles =
]
cellStyles : List Style
cellStyles =
[ verticalAlign middle
]
loadingContentStyles : List Style
loadingContentStyles =
[ width (pct 100)

285
src/Nri/Ui/Table/V5.elm Normal file
View File

@ -0,0 +1,285 @@
module Nri.Ui.Table.V5 exposing
( Column, custom, string
, view, viewWithoutHeader
, viewLoading, viewLoadingWithoutHeader
)
{-| Upgrading from V1:
- All the `width` fields in column configurations now take an elm-css length
value rather than an Integer. Change `width = 100` to `width = px 100` to get
the same widths as before.
- Tables now by default take the full width of the container they are placed in.
If this is not what you want, wrap the table in an element with a fixed width.
- The table module now makes use of `Html.Styled` and no longer exposes a
separate `styles` value.
Check out the [elm-css](http://package.elm-lang.org/packages/rtfeldman/elm-css/14.0.0/Html-Styled)
documentation on Html.Styled to see how to work with it.
- The default cell padding has been removed and content is not vertically
centered in its cell. If you need to overwrite this, wrap your cells in
elements providing custom styling to the cell.
@docs Column, custom, string
@docs view, viewWithoutHeader
@docs viewLoading, viewLoadingWithoutHeader
-}
import Css exposing (..)
import Css.Animations
import Html.Styled as Html exposing (..)
import Html.Styled.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)
{-| A column that renders some aspect of a value as text
-}
string :
{ header : String
, value : data -> String
, width : LengthOrAuto compatible
, cellStyles : data -> List Style
}
-> Column data msg
string { header, value, width, cellStyles } =
Column (Html.text header) (value >> Html.text) (Css.width width) cellStyles
{-| A column that renders however you want it to
-}
custom :
{ header : Html msg
, view : data -> Html msg
, width : LengthOrAuto compatible
, cellStyles : data -> List Style
}
-> Column data msg
custom options =
Column options.header options.view (Css.width options.width) options.cellStyles
-- VIEW
{-| Displays a table of data without a header row
-}
viewWithoutHeader : List (Column data msg) -> List data -> Html msg
viewWithoutHeader columns =
tableWithoutHeader [] columns (viewRow columns)
{-| Displays a table of data based on the provided column definitions
-}
view : List (Column data msg) -> List data -> Html msg
view columns =
tableWithHeader [] 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) =
td
[ 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 (Column data msg) -> Html msg
viewLoading columns =
tableWithHeader loadingTableStyles columns (viewLoadingRow columns) (List.range 0 8)
{-| Display the loading table without a header row
-}
viewLoadingWithoutHeader : List (Column data msg) -> Html msg
viewLoadingWithoutHeader columns =
tableWithoutHeader loadingTableStyles 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 _) =
td
[ 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
[ 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 tableRowHeader columns)
]
tableRowHeader : Column data msg -> Html msg
tableRowHeader (Column header _ width _) =
th
[ css (width :: headerStyles)
]
[ header ]
tableBody : (a -> Html msg) -> List a -> Html msg
tableBody toRow items =
tbody [] (List.map toRow items)
-- STYLES
headersStyles : List Style
headersStyles =
[ borderBottom3 (px 3) solid gray75
, height (px 45)
, fontSize (px 15)
]
headerStyles : List Style
headerStyles =
[ padding4 (px 15) (px 12) (px 11) (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)
]