Skip to content

Commit 7c8c62e

Browse files
authored
Add custom remove cell prop to EditableTable (#46)
* WIP: Make the removeCell in an EditableTable customizable * Handle non-removable cells in removeCell prop * Add dropdownIcon, allowing borderless icon buttons (#43) * Add dropdownIcon, allowing borderless icon buttons * Use Icon for dropdownIconDefaults
1 parent 72c05ab commit 7c8c62e

File tree

4 files changed

+194
-80
lines changed

4 files changed

+194
-80
lines changed

docs/Examples/DropdownButton.example.purs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ import Effect.Console (log)
99
import Lumi.Components.Button (button, defaults, secondary)
1010
import Lumi.Components.Color (colors)
1111
import Lumi.Components.Column (column, column_)
12-
import Lumi.Components.DropdownButton (dropdownButton, dropdownMenu, dropdownButtonDefaults, dropdownMenuDefaults)
12+
import Lumi.Components.DropdownButton (dropdownButton, dropdownButtonDefaults, dropdownIcon, dropdownIconDefaults, dropdownMenu, dropdownMenuDefaults)
13+
import Lumi.Components.Example (example)
1314
import Lumi.Components.Input (input, text_)
1415
import Lumi.Components.InputGroup (inputGroup)
1516
import Lumi.Components.Spacing (Space(..), vspace)
1617
import Lumi.Components.Text (body_, h2_)
17-
import Lumi.Components.Example (example)
1818
import React.Basic (JSX)
1919
import React.Basic.DOM as R
2020

@@ -197,4 +197,27 @@ docs =
197197
"""
198198
]
199199
}
200+
201+
, h2_ "using an arbitrary icon in place of the button"
202+
, example $
203+
dropdownIcon dropdownIconDefaults
204+
{ content = R.div
205+
{ style: R.css { width: "328px", padding: "12px" }
206+
, children:
207+
[ button secondary
208+
{ title = "I can be any element"
209+
, style = R.css { width: "100%", marginBottom: "8px" }
210+
}
211+
, button secondary
212+
{ title = "I can be any element"
213+
, style = R.css { width: "100%", marginBottom: "8px" }
214+
}
215+
, button defaults
216+
{ title = "I can be any element"
217+
, style = R.css { width: "100%" }
218+
}
219+
]
220+
}
221+
}
222+
200223
]

docs/Examples/EditableTable.example.purs

Lines changed: 102 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,30 @@ import Prelude
55
import Data.Array.NonEmpty (filter, fromArray, fromNonEmpty, length, snoc)
66
import Data.BigInt (fromInt, fromString, toNumber, toString)
77
import Data.Either (Either(..))
8-
import Data.Foldable (sum)
8+
import Data.Foldable (sum, traverse_)
9+
import Data.Foldable as Array
910
import Data.Maybe (Maybe(..), fromMaybe)
1011
import Data.NonEmpty ((:|))
12+
import Data.Nullable as Nullable
1113
import Data.Number (isNaN)
1214
import Data.Number.Format (fixed, toStringWith)
15+
import Effect.Console (log)
1316
import Global (readInt)
1417
import Lumi.Components.Column (column_)
15-
import Lumi.Components.EditableTable (editableTable)
18+
import Lumi.Components.DropdownButton (dropdownIcon, dropdownIconDefaults)
19+
import Lumi.Components.EditableTable (editableTable, editableTableDefaults)
20+
import Lumi.Components.Example (example)
21+
import Lumi.Components.Input (CheckboxState(..), alignToInput, input, switch)
1622
import Lumi.Components.Input as Input
23+
import Lumi.Components.Link as Link
1724
import Lumi.Components.Row (row_)
18-
import Lumi.Components.Text (body, body_, nbsp, text)
19-
import Lumi.Components.Example (example)
25+
import Lumi.Components.Spacing (Space(..), hspace)
26+
import Lumi.Components.Text (body, body_, nbsp, p_, text)
2027
import React.Basic (Component, JSX, createComponent, make)
2128
import React.Basic.DOM as R
22-
import React.Basic.DOM.Events (targetValue)
29+
import React.Basic.DOM.Events (stopPropagation, targetChecked, targetValue)
2330
import React.Basic.Events (handler)
31+
import React.Basic.Events (handler) as Events
2432

2533
component :: Component Unit
2634
component = createComponent "EditableTableExample"
@@ -40,63 +48,72 @@ docs = unit # make component
4048
, price: 0.23
4149
}
4250
]
51+
, customRemoveCell: false
4352
}
4453

4554
, render: \self ->
46-
column_
47-
[ example $
48-
editableTable
49-
{ addLabel: "Add another row"
50-
, columns:
51-
[ { label: "Description"
52-
, renderCell: \row -> Input.input Input.text_
53-
{ value = row.description
54-
, onChange = handler targetValue \value ->
55-
updateRow self row { description = fromMaybe row.description value }
56-
, style = R.css { width: "100%" }
57-
}
58-
}
59-
, { label: "Quantity"
60-
, renderCell: \row -> Input.input Input.number
61-
{ value = toString row.quantity
62-
, onChange = handler targetValue \value ->
63-
updateRow self row { quantity = fromMaybe row.quantity $ fromString =<< value }
64-
}
65-
}
66-
, { label: "Price"
67-
, renderCell: \row -> Input.input Input.number
68-
{ value = show row.price
69-
, onChange = handler targetValue \value ->
70-
updateRow self $ fromMaybe row do
71-
value' <- readInt 10 <$> value
72-
pure if isNaN value'
73-
then row
74-
else row { price = value' }
75-
}
76-
}
77-
, { label: "Total"
78-
, renderCell: \row -> R.text $ toMoneyString (calculateTotal row)
79-
}
55+
column_ $ pure $ example $ column_
56+
[ row_
57+
[ alignToInput $ body_ "Custom remove cell?"
58+
, input switch
59+
{ checked = if self.state.customRemoveCell then On else Off
60+
, onChange = Events.handler (stopPropagation >>> targetChecked)
61+
(traverse_ (\checked -> self.setState (_ { customRemoveCell = checked })))
62+
}
63+
]
64+
, editableTable
65+
{ addLabel: "Add another row"
66+
, columns:
67+
[ { label: "Description"
68+
, renderCell: \row -> Input.input Input.text_
69+
{ value = row.description
70+
, onChange = handler targetValue \value ->
71+
updateRow self row { description = fromMaybe row.description value }
72+
, style = R.css { width: "100%" }
73+
}
74+
}
75+
, { label: "Quantity"
76+
, renderCell: \row -> Input.input Input.number
77+
{ value = toString row.quantity
78+
, onChange = handler targetValue \value ->
79+
updateRow self row { quantity = fromMaybe row.quantity $ fromString =<< value }
80+
}
81+
}
82+
, { label: "Price"
83+
, renderCell: \row -> Input.input Input.number
84+
{ value = show row.price
85+
, onChange = handler targetValue \value ->
86+
updateRow self $ fromMaybe row do
87+
value' <- readInt 10 <$> value
88+
pure if isNaN value'
89+
then row
90+
else row { price = value' }
91+
}
92+
}
93+
, { label: "Total"
94+
, renderCell: \row -> R.text $ toMoneyString (calculateTotal row)
95+
}
96+
]
97+
, maxRows: 5
98+
, onRowAdd: addRow self
99+
, onRowRemove: removeRow self
100+
, removeCell: removeCell self
101+
, readonly: false
102+
, rowEq: eq
103+
, rows: Right self.state.rows
104+
, summary:
105+
row_
106+
[ text body
107+
{ children = [ R.text "Total:" ]
108+
, style = R.css { fontWeight: "bold" }
109+
}
110+
, body_ nbsp
111+
, body_ nbsp
112+
, body_ nbsp
113+
, body_ nbsp
114+
, body_ $ toMoneyString $ sum $ calculateTotal <$> self.state.rows
80115
]
81-
, maxRows: 5
82-
, onRowAdd: addRow self
83-
, onRowRemove: removeRow self
84-
, readonly: false
85-
, rowEq: eq
86-
, rows: Right self.state.rows
87-
, summary:
88-
row_
89-
[ text body
90-
{ children = [ R.text "Total:" ]
91-
, style = R.css { fontWeight: "bold" }
92-
}
93-
, body_ nbsp
94-
, body_ nbsp
95-
, body_ nbsp
96-
, body_ nbsp
97-
, body_ $ toMoneyString $ sum $ calculateTotal <$> self.state.rows
98-
]
99-
}
116+
}
100117
]
101118
}
102119
where
@@ -129,3 +146,30 @@ docs = unit # make component
129146

130147
toMoneyString value =
131148
"$" <> toStringWith (fixed 2) value
149+
150+
removeCell self =
151+
if self.state.customRemoveCell
152+
then \onRemove row ->
153+
row_
154+
[ hspace S16
155+
, dropdownIcon dropdownIconDefaults
156+
{ alignment = Nullable.notNull "right"
157+
, content = R.div
158+
{ style: R.css { width: "328px", padding: "12px" }
159+
, children:
160+
[ Link.link Link.defaults
161+
{ className = pure "lumi-dropdown-menu-item"
162+
, text = p_ "Do something with this row"
163+
, navigate = pure $ log $ "Did something: " <> show row
164+
}
165+
, onRemove # Array.foldMap \onRemove' ->
166+
Link.link Link.defaults
167+
{ className = pure "lumi-dropdown-menu-item"
168+
, text = p_ "Remove this row"
169+
, navigate = pure $ onRemove' row
170+
}
171+
]
172+
}
173+
}
174+
]
175+
else editableTableDefaults.removeCell

src/Lumi/Components/DropdownButton.purs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import Lumi.Components.Button (button, secondary)
1515
import Lumi.Components.Color (colors)
1616
import Lumi.Components.Column (column_)
1717
import Lumi.Components.Divider (divider_)
18+
import Lumi.Components.Icon as Icon
1819
import Lumi.Components.Link as Link
1920
import Lumi.Components.Text (p_)
2021
import Lumi.Components.ZIndex (ziDropdownButton)
@@ -25,6 +26,7 @@ import React.Basic.DOM.Components.GlobalEvents (windowEvent)
2526
import React.Basic.DOM.Components.Ref (ref)
2627
import React.Basic.DOM.Events (stopPropagation)
2728
import React.Basic.Events (handler, handler_)
29+
import Unsafe.Coerce (unsafeCoerce)
2830
import Unsafe.Reference (unsafeRefEq)
2931
import Web.DOM (Node)
3032
import Web.DOM.Document (createElement) as DOM
@@ -232,6 +234,36 @@ dropdownMenuDefaults =
232234
, items: []
233235
}
234236

237+
type DropdownIconProps =
238+
{ icon :: JSX
239+
, content :: JSX
240+
, onOpen :: Effect Unit
241+
, alignment :: Nullable String
242+
}
243+
244+
dropdownIconDefaults :: DropdownIconProps
245+
dropdownIconDefaults =
246+
{ icon: Icon.icon
247+
{ type_: Icon.Overflow
248+
, style: R.css { color: cssStringHSLA colors.black1 }
249+
}
250+
, content: mempty
251+
, onOpen: pure unit
252+
, alignment: toNullable Nothing
253+
}
254+
255+
dropdownIcon :: DropdownIconProps -> JSX
256+
dropdownIcon props =
257+
dropdownButton
258+
-- this `unsafeCoerce` is a hack until Dropdown and Button
259+
-- both support JSX labels instead of Strings
260+
{ label: (unsafeCoerce :: JSX -> String) props.icon
261+
, content: props.content
262+
, className: "lumi-dropdown-icon"
263+
, onOpen: props.onOpen
264+
, alignment: props.alignment
265+
}
266+
235267
foreign import checkIsEventTargetInTree :: EffectFn2 Node Event Boolean
236268

237269
getAbsolutePosition :: HTML.HTMLElement -> Effect { bottom :: Number, left :: Number, right :: Number }
@@ -253,7 +285,9 @@ styles = jss
253285
{ "lumi-dropdown-button":
254286
{ display: "inline-block"
255287
, position: "relative"
256-
, "& > react-basic-ref > button.lumi":
288+
}
289+
, "lumi-dropdown-button:not(.lumi-dropdown-icon)":
290+
{ "& > react-basic-ref > button.lumi":
257291
{ backgroundImage: "url(\"data:image/svg+xml;charset=utf8,%3C?xml version='1.0' encoding='UTF-8'?%3E%3Csvg width='11px' height='5px' viewBox='0 0 11 5' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3C!-- Generator: Sketch 49.1 (51147) - http://www.bohemiancoding.com/sketch --%3E%3Ctitle%3ESlice 1%3C/title%3E%3Cdesc%3ECreated with Sketch.%3C/desc%3E%3Cdefs%3E%3Cpath d='M5.417,3.519 C5.797,3.187 6.307,2.733 6.912,2.185 L6.974,2.129 C7.70584693,1.46645784 8.43485361,0.800785071 9.161,0.132 C9.29247374,0.0108869595 9.47857352,-0.0308857001 9.64919735,0.0224173781 C9.81982119,0.0757204563 9.94904726,0.216001266 9.98819736,0.390417384 C10.0273475,0.564833501 9.97047374,0.746886966 9.839,0.868 C9.11068658,1.53896706 8.37934578,2.20664054 7.645,2.871 L7.583,2.927 C5.376,4.922 5.287,5 5,5 C4.713,5 4.624,4.922 2.417,2.927 L2.355,2.871 C1.62081041,2.20646869 0.889470368,1.5387959 0.161,0.868 C-0.0422407879,0.68077547 -0.0552245302,0.364240788 0.132,0.161 C0.31922453,-0.0422407879 0.635759212,-0.0552245302 0.839,0.132 C1.5649901,0.800955473 2.29399753,1.46662893 3.026,2.129 L3.088,2.185 C3.693,2.733 4.203,3.187 4.583,3.519 C4.75,3.665 4.89,3.785 5,3.877 C5.11,3.785 5.25,3.665 5.417,3.519 Z' id='path-1'%3E%3C/path%3E%3C/defs%3E%3Cg id='Page-1' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='arrow-down'%3E%3Cg id='a-link' fill='%2342413F' fill-rule='nonzero'%3E%3Cpath d='M5.417,3.519 C5.797,3.187 6.307,2.733 6.912,2.185 L6.974,2.129 C7.70584693,1.46645784 8.43485361,0.800785071 9.161,0.132 C9.29247374,0.0108869595 9.47857352,-0.0308857001 9.64919735,0.0224173781 C9.81982119,0.0757204563 9.94904726,0.216001266 9.98819736,0.390417384 C10.0273475,0.564833501 9.97047374,0.746886966 9.839,0.868 C9.11068658,1.53896706 8.37934578,2.20664054 7.645,2.871 L7.583,2.927 C5.376,4.922 5.287,5 5,5 C4.713,5 4.624,4.922 2.417,2.927 L2.355,2.871 C1.62081041,2.20646869 0.889470368,1.5387959 0.161,0.868 C-0.0422407879,0.68077547 -0.0552245302,0.364240788 0.132,0.161 C0.31922453,-0.0422407879 0.635759212,-0.0552245302 0.839,0.132 C1.5649901,0.800955473 2.29399753,1.46662893 3.026,2.129 L3.088,2.185 C3.693,2.733 4.203,3.187 4.583,3.519 C4.75,3.665 4.89,3.785 5,3.877 C5.11,3.785 5.25,3.665 5.417,3.519 Z' id='a'%3E%3C/path%3E%3C/g%3E%3Cg id='Clipped'%3E%3Cmask id='mask-2' fill='white'%3E%3Cuse xlink:href='%23path-1'%3E%3C/use%3E%3C/mask%3E%3Cg id='a'%3E%3C/g%3E%3Cg id='Group' mask='url(%23mask-2)' fill='%23292827' fill-rule='nonzero'%3E%3Cg transform='translate(-5.000000, -8.000000)' id='Shape'%3E%3Cpolygon points='0 0 20 0 20 20 0 20'%3E%3C/polygon%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E\")"
258292
, backgroundRepeat: "no-repeat"
259293
, backgroundPositionY: "center"
@@ -266,6 +300,13 @@ styles = jss
266300
}
267301
}
268302
}
303+
, "lumi-dropdown-button.lumi-dropdown-icon":
304+
{ "& > react-basic-ref > button.lumi":
305+
{ border: "none"
306+
, padding: "0"
307+
, minWidth: "0"
308+
}
309+
}
269310

270311
, "lumi-dropdown-button-content":
271312
{ cursor: "default"

0 commit comments

Comments
 (0)