Elastic Date Picker
A range of time that can move with the clock, as a dashboard is filtered by: "the last 15 minutes" is still the last 15 minutes at the next refresh. Elastic UI's EuiSuperDatePicker, built again on Pantrix.
import com.pantrixui.components.date.elasticdatepicker.PantrixElasticDatePicker
- Field. The quick menu's calendar at the start, the range's name in the middle ("Last 15 minutes", "Today"), Refresh beside it. A press on the name shows the two ends (start → end) and opens the start's panel. A range with no name shows its two ends straight away.
- Quick menu. The last or the next amount of a unit, ‹ and › that move the range by its own length, the ranges
commonlyUsedoffers by name (by default Elastic UI's eight) and thoserecentlyUsedholds. A choice there is handed over at once. - An end. Three tabs: Absolute (a calendar, the times by the half hour and a moment to type), Relative (an amount, a unit, the moment it falls on, rounding to the unit), Now. Moving to a tab makes the end what that tab shows. Apply at the end of the moment's field on Absolute and Relative takes the edit and closes the panel; Now's button makes the end now and does the same.
- An edit that waits. The field is painted in the success colour, a mark appears at its end and the button beside it says Update. An end before the start paints the field in the danger colour and Update takes nothing. With
updateButton = Noneeach edit is handed over at once. - One press. With a panel open, a press on the other end, on Update or anywhere else on the page closes the panel and does what it presses, as in Elastic UI. A press on the open end's button closes its panel.
- Popover, sheet or dialog.
presentationsays what each panel opens in; the same on every platform. The default is a sheet in a narrow window and a popover in a wider one. The panels have no title over them. - Close-set panels. As Elastic UI's: an end's panel is as wide as its calendar and the times beside it, the quick menu 400dp; the fields, tabs and buttons in them are 8dp lower than a form's.
- Auto refresh.
autoRefreshandonAutoRefreshChange: "Refresh every" in the quick menu, and how often at the field's end;minAutoRefreshthe shortest. Each turn waits foronRefresh, and one that fails stops the refresh. - Time window.
timeWindowButtons: ‹ and › move the range by its length, and zoom out, and zoom in where asked for, widen and narrow it byzoomFactor. - The field's layout.
layout: the range, the quick menu's button alone, or auto refresh alone. - The quick menu's parts.
customQuickPanelsfor panels of its own,quickMenufor an order of its own. - Time zone and disabled words.
utcOffsetis the moments' difference,timeZoneNamethe name in the panels;disabledTextthe words of a picker that takes no choice. - Time. The ends are
PantrixTimePoints;PantrixTimeRange.resolve()makes them twoInstants by now, andtoDateMath()writes them in Elasticsearch's date math. More in docs/guide/date-time/DATE_TIME_GUIDE.md.
Examples
A dashboard with the ranges used lately
The last 15 minutes of a dashboard. The ranges chosen gather under the quick menu's "Recently used"; Refresh keeps a load going for a second, and the button spins meanwhile. Under it, the two moments the range falls on now and the time of the last refresh.
var range by remember { mutableStateOf(LAST_QUARTER) }
var refreshed by remember { mutableStateOf<Instant?>(null) }
val report = LocalExampleEvents.current
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
PantrixElasticDatePicker(
range = range,
onRangeChange = {
range = it
refreshed = Clock.System.now()
report("Range chosen")
},
recentlyUsed = rememberRecentRanges(),
onRefresh = {
delay(LOADING_MILLIS)
refreshed = Clock.System.now()
report("Refreshed")
},
tagKey = "dashboard"
)
Resolved(range)
refreshed?.let { PantrixText(text = "Last refresh: ${moment(it)}", style = PantrixTheme.typography.footnote) }
}
@Composable
private fun Resolved(range: PantrixTimeRange) {
val read = range.resolve()
PantrixText(
text = read?.let { "${moment(it.start)} – ${moment(it.endInclusive)}" } ?: "The end comes before the start",
style = PantrixTheme.typography.footnote,
color = PantrixTheme.colors.textSecondary
)
}
@Composable
private fun moment(at: Instant): String {
val strings = PantrixTheme.strings
val day = PantrixDate.of(at)
val time = PantrixTime.of(at)
return "${strings.date(day.day, day.month, day.year)} ${strings.clock(time.hour, time.minute, time.second)}"
}
private val LAST_QUARTER = last(15, PantrixTimeUnit.Minutes)
private const val LOADING_MILLIS = 1000L
private fun last(
amount: Int,
unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)
Update button: in words, icon alone, none
The button beside the field three ways. In words it says Update while an edit waits; the icon alone does the same in less room; with no button each edit in an end is handed over at once. Change an end and see the difference.
var button by remember { mutableStateOf(ElasticDatePickerUpdateButton.Text) }
var range by remember { mutableStateOf(LAST_QUARTER) }
val report = LocalExampleEvents.current
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
PantrixSegmentButton(
options = listOf(
SegmentOption(ElasticDatePickerUpdateButton.Text, "In words", tagKey = "text"),
SegmentOption(ElasticDatePickerUpdateButton.Icon, "Icon alone", tagKey = "icon"),
SegmentOption(ElasticDatePickerUpdateButton.None, "No button", tagKey = "none")
),
selected = button,
onSelect = { button = it },
fullWidth = false,
tagKey = "update_button"
)
PantrixElasticDatePicker(
range = range,
onRangeChange = {
range = it
report("Range handed over")
},
updateButton = button,
tagKey = "update"
)
Resolved(range)
}
@Composable
private fun Resolved(range: PantrixTimeRange) {
val read = range.resolve()
PantrixText(
text = read?.let { "${moment(it.start)} – ${moment(it.endInclusive)}" } ?: "The end comes before the start",
style = PantrixTheme.typography.footnote,
color = PantrixTheme.colors.textSecondary
)
}
private val LAST_QUARTER = last(15, PantrixTimeUnit.Minutes)
@Composable
private fun moment(at: Instant): String {
val strings = PantrixTheme.strings
val day = PantrixDate.of(at)
val time = PantrixTime.of(at)
return "${strings.date(day.day, day.month, day.year)} ${strings.clock(time.hour, time.minute, time.second)}"
}
private fun last(
amount: Int,
unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)
Popover, sheet or dialog
Where the panels open is the same on every platform: with presentation = Popover a popover beside the part, with presentation = Sheet a sheet from the edge of the screen, with presentation = Dialog a dialog in the middle of the window, as wide as the panel. The default is Adaptive: a sheet in a narrow window, a popover in a wider one; switch between "Narrow" and "Wide" in the card's window selector. None has a title over it. The quick menu, the start and the end all open the way chosen.
var presentation by remember { mutableStateOf(DatePickerPresentation.Sheet) }
val today = PantrixQuickRange.commonlyUsed(PantrixTheme.strings).first().range
var range by remember { mutableStateOf(today) }
// The card's window is as tall as the example: the example is kept tall so that a sheet opened in a narrow window has room.
Column(
modifier = Modifier.fillMaxWidth().height(480.dp),
verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)
) {
PantrixSegmentButton(
options = listOf(
SegmentOption(DatePickerPresentation.Popover, "Popover", tagKey = "popover"),
SegmentOption(DatePickerPresentation.Sheet, "Sheet", tagKey = "sheet"),
SegmentOption(DatePickerPresentation.Dialog, "Dialog", tagKey = "dialog"),
SegmentOption(DatePickerPresentation.Adaptive, "Adaptive", tagKey = "adaptive")
),
selected = presentation,
onSelect = { presentation = it },
fullWidth = false,
tagKey = "presentation"
)
PantrixElasticDatePicker(
range = range,
onRangeChange = { range = it },
presentation = presentation,
tagKey = "presentation"
)
}
The quick menu's own ranges
The quick menu's ranges are the app's: here seven ranges from the last 15 minutes to the last year in place of Elastic UI's eight. Switching "Recently used" shows or hides the list; a range with a name stands in the field by it.
val ranges = remember {
listOf(
PantrixQuickRange("Last 15 minutes", last(15, PantrixTimeUnit.Minutes)),
PantrixQuickRange("Last 1 hour", last(1, PantrixTimeUnit.Hours)),
PantrixQuickRange("Last 24 hours", last(24, PantrixTimeUnit.Hours)),
PantrixQuickRange("Last 7 days", last(7, PantrixTimeUnit.Days)),
PantrixQuickRange("Last 30 days", last(30, PantrixTimeUnit.Days)),
PantrixQuickRange("Last 90 days", last(90, PantrixTimeUnit.Days)),
PantrixQuickRange("Last 1 year", last(1, PantrixTimeUnit.Years))
)
}
var range by remember { mutableStateOf(ranges[4].range) }
var showRecent by remember { mutableStateOf(true) }
val recent = rememberRecentRanges(max = 5)
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
PantrixSwitchButton(
checked = showRecent,
onCheckedChange = { showRecent = it },
label = "Recently used",
tagKey = "show_recent"
)
PantrixElasticDatePicker(
range = range,
onRangeChange = { range = it },
commonlyUsed = ranges,
recentlyUsed = recent.takeIf { showRecent },
tagKey = "own_ranges"
)
}
private fun last(
amount: Int,
unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)
Width and height
The picker fills the width it is given up to the style's maxWidth (560dp by default, Elastic UI's "restricted"). maxWidth = Dp.Infinity gives the full width; in a narrow place the button in words is its icon alone; height makes the field compressed.
var range by remember { mutableStateOf(SINCE_YESTERDAY) }
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.md)) {
PantrixText(text = "Default (up to 560dp)", style = PantrixTheme.typography.footnote)
PantrixElasticDatePicker(range = range, onRangeChange = { range = it }, tagKey = "restricted")
PantrixText(text = "Full width", style = PantrixTheme.typography.footnote)
PantrixElasticDatePicker(
range = range,
onRangeChange = { range = it },
style = ElasticDatePickerStyle(maxWidth = Dp.Infinity),
tagKey = "full"
)
PantrixText(text = "In a narrow place (320dp)", style = PantrixTheme.typography.footnote)
PantrixElasticDatePicker(
range = range,
onRangeChange = { range = it },
modifier = Modifier.width(NARROW_WIDTH),
tagKey = "narrow"
)
PantrixText(text = "Compressed (32dp)", style = PantrixTheme.typography.footnote)
PantrixElasticDatePicker(
range = range,
onRangeChange = { range = it },
style = ElasticDatePickerStyle(height = COMPRESSED_HEIGHT),
tagKey = "compressed"
)
}
private val SINCE_YESTERDAY = PantrixTimeRange(
PantrixTimePoint.Relative(-1, PantrixTimeUnit.Days, roundTo = PantrixTimeUnit.Days),
PantrixTimePoint.Now
)
private val NARROW_WIDTH = 320.dp
private val COMPRESSED_HEIGHT = 32.dp
Loading and disabled
A load and a picker that takes no choice. While loading, the button beside the field spins and takes no press; a disabled picker is drawn faint and opens no panel.
var range by remember { mutableStateOf(LAST_HOUR) }
var loading by remember { mutableStateOf(true) }
var enabled by remember { mutableStateOf(true) }
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
PantrixSwitchButton(
checked = loading,
onCheckedChange = { loading = it },
label = "Loading",
tagKey = "loading"
)
PantrixSwitchButton(
checked = enabled,
onCheckedChange = { enabled = it },
label = "Enabled",
tagKey = "enabled"
)
PantrixElasticDatePicker(
range = range,
onRangeChange = { range = it },
loading = loading,
enabled = enabled,
tagKey = "states"
)
}
private val LAST_HOUR = last(1, PantrixTimeUnit.Hours)
private fun last(
amount: Int,
unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)
The days that can be chosen
The calendar offers this month's days alone (minDate and maxDate): on an end's Absolute tab the days outside the month are faint and cannot be chosen. A relative end is not bound by them.
val today = rememberToday()
val first = PantrixDate(today.year, today.month, 1)
val last = first.plusMonths(1).plusDays(-1)
var range by remember {
mutableStateOf(PantrixTimeRange(PantrixTimePoint.Absolute(first.at(PantrixTime(0, 0))), PantrixTimePoint.Now))
}
PantrixElasticDatePicker(
range = range,
onRangeChange = { range = it },
minDate = first,
maxDate = last,
tagKey = "bounds"
)
private fun last(
amount: Int,
unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)
Clock, start of the week and units
How moments are written and read: 12 or 24 hours (is24Hour), the first day of the week (firstDayOfWeek, which sets the /w rounding too), and whether a relative end is put in the unit that reads best (roundRelativeUnits: 90 minutes "~ 2 hours ago" or "90 minutes ago"). The language comes from the page's language button.
var is24Hour by remember { mutableStateOf(true) }
var mondayFirst by remember { mutableStateOf(true) }
var roundUnits by remember { mutableStateOf(true) }
var range by remember { mutableStateOf(NINETY_TO_THIRTY_AGO) }
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
PantrixSwitchButton(checked = is24Hour, onCheckedChange = { is24Hour = it }, label = "24 hours", tagKey = "clock")
PantrixSwitchButton(
checked = mondayFirst,
onCheckedChange = { mondayFirst = it },
label = "The week starts on Monday",
tagKey = "monday"
)
PantrixSwitchButton(
checked = roundUnits,
onCheckedChange = { roundUnits = it },
label = "Round the units",
tagKey = "round"
)
PantrixElasticDatePicker(
range = range,
onRangeChange = { range = it },
roundRelativeUnits = roundUnits,
firstDayOfWeek = if (mondayFirst) 1 else SUNDAY,
is24Hour = is24Hour,
tagKey = "clock"
)
}
private val NINETY_TO_THIRTY_AGO = PantrixTimeRange(
PantrixTimePoint.Relative(-90, PantrixTimeUnit.Minutes),
PantrixTimePoint.Relative(-30, PantrixTimeUnit.Minutes)
)
private const val SUNDAY = 7
Date math in an address
The range stands in an address as date math: from and to are written again at each choice. With an end typed into the fields and Apply pressed, the range is read from them; an end that cannot be read draws its field as wrong.
var range by remember { mutableStateOf(SINCE_YESTERDAY) }
val written = range.toDateMath()
var from by remember(written) { mutableStateOf(written.first) }
var to by remember(written) { mutableStateOf(written.second) }
val start = PantrixTimePoint.parseDateMath(from)
val end = PantrixTimePoint.parseDateMath(to)
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
PantrixElasticDatePicker(range = range, onRangeChange = { range = it }, tagKey = "url")
PantrixText(text = "?from=${written.first}&to=${written.second}", style = PantrixTheme.typography.footnote)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm),
verticalAlignment = Alignment.Bottom
) {
PantrixTextField(
value = from,
onValueChange = { from = it },
modifier = Modifier.weight(1f),
label = "from",
error = start == null,
showCharacterCount = false,
tagKey = "from"
)
PantrixTextField(
value = to,
onValueChange = { to = it },
modifier = Modifier.weight(1f),
label = "to",
error = end == null,
showCharacterCount = false,
tagKey = "to"
)
}
PantrixButton(
text = "Apply",
onClick = { if (start != null && end != null) range = PantrixTimeRange(start, end) },
variant = ButtonVariant.Outline,
enabled = start != null && end != null,
tagKey = "read"
)
}
private val SINCE_YESTERDAY = PantrixTimeRange(
PantrixTimePoint.Relative(-1, PantrixTimeUnit.Days, roundTo = PantrixTimeUnit.Days),
PantrixTimePoint.Now
)
Colours of its own
The field's own colours: its ground, edge, words and icons, the open end's edge, and the colours of an edit that waits and of ends out of order. Each colour not given is the theme's. Change an end and look at the waiting colour.
var range by remember { mutableStateOf(LAST_QUARTER) }
val colors = PantrixTheme.colors
PantrixElasticDatePicker(
range = range,
onRangeChange = { range = it },
colors = ElasticDatePickerColors(
container = colors.neutral.container,
border = colors.neutral.color,
active = colors.warning.color,
pendingContainer = colors.info.container,
pendingContent = colors.info.onContainer
),
tagKey = "colors"
)
private val LAST_QUARTER = last(15, PantrixTimeUnit.Minutes)
private fun last(
amount: Int,
unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)
Auto refresh
The dashboard refreshes itself every ten seconds: the field ends in how often, written short, and a press on it opens the quick menu. The menu's "Refresh every" part turns it on and off and changes how often; nothing shorter than five seconds is taken (minAutoRefresh). onRefresh runs at each turn, and the next turn waits for it to end.
var range by remember { mutableStateOf(LAST_QUARTER) }
var refresh by remember { mutableStateOf(PantrixAutoRefresh(every = 10, paused = false)) }
var count by remember { mutableStateOf(0) }
val report = LocalExampleEvents.current
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
PantrixElasticDatePicker(
range = range,
onRangeChange = { range = it },
onRefresh = {
count++
report("Refreshed")
},
tagKey = "auto_refresh",
autoRefresh = refresh,
onAutoRefreshChange = {
refresh = it
report(if (it.paused) "Auto refresh stopped" else "Auto refresh changed")
},
minAutoRefresh = 5.seconds
)
PantrixText(text = "Refreshes: $count", style = PantrixTheme.typography.footnote)
}
private val LAST_QUARTER = last(15, PantrixTimeUnit.Minutes)
private fun last(
amount: Int,
unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)
Auto refresh only
Auto refresh alone, for a page with no time data (layout = AutoRefreshOnly): the field says "Auto refresh" and how often or off, and a press opens the menu that sets it alone. How often is given in minutes here (unit = AutoRefreshUnit.Minutes): once a minute.
var refresh by remember { mutableStateOf(PantrixAutoRefresh(every = 1, unit = AutoRefreshUnit.Minutes)) }
var count by remember { mutableStateOf(0) }
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
PantrixElasticDatePicker(
range = LAST_QUARTER,
onRangeChange = { count++ },
tagKey = "auto_refresh_only",
autoRefresh = refresh,
onAutoRefreshChange = { refresh = it },
layout = ElasticDatePickerLayout.AutoRefreshOnly
)
PantrixText(text = "Refreshes: $count", style = PantrixTheme.typography.footnote)
}
private val LAST_QUARTER = last(15, PantrixTimeUnit.Minutes)
private fun last(
amount: Int,
unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)
Quick select only
For a page that shows the range somewhere else, the quick menu's button alone with the button beside it (layout = QuickSelectOnly). The range chosen is written under it.
var range by remember { mutableStateOf(LAST_QUARTER) }
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
PantrixElasticDatePicker(
range = range,
onRangeChange = { range = it },
tagKey = "quick_select_only",
layout = ElasticDatePickerLayout.QuickSelectOnly
)
Resolved(range)
}
@Composable
private fun Resolved(range: PantrixTimeRange) {
val read = range.resolve()
PantrixText(
text = read?.let { "${moment(it.start)} – ${moment(it.endInclusive)}" } ?: "The end comes before the start",
style = PantrixTheme.typography.footnote,
color = PantrixTheme.colors.textSecondary
)
}
private val LAST_QUARTER = last(15, PantrixTimeUnit.Minutes)
@Composable
private fun moment(at: Instant): String {
val strings = PantrixTheme.strings
val day = PantrixDate.of(at)
val time = PantrixTime.of(at)
return "${strings.date(day.day, day.month, day.year)} ${strings.clock(time.hour, time.minute, time.second)}"
}
private fun last(
amount: Int,
unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)
Time window buttons
Elastic UI's time window buttons beside the field: ‹ and › move the range by its own length, zoom out widens it by zoomFactor of its length and zoom in narrows it by as much; zoom in is there only when it is asked for. The range they hand over is of moments.
var range by remember { mutableStateOf(LAST_HOUR) }
var zoomIn by remember { mutableStateOf(false) }
var factor by remember { mutableStateOf(HALF) }
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
PantrixSwitchButton(checked = zoomIn, onCheckedChange = { zoomIn = it }, label = "Zoom in", tagKey = "zoom_in")
PantrixSegmentButton(
options = listOf(
SegmentOption(HALF, "Half (0.5)", tagKey = "half"),
SegmentOption(QUARTER, "A quarter (0.25)", tagKey = "quarter")
),
selected = factor,
onSelect = { factor = it },
fullWidth = false,
tagKey = "zoom_factor"
)
PantrixElasticDatePicker(
range = range,
onRangeChange = { range = it },
tagKey = "time_window",
timeWindowButtons = ElasticTimeWindowButtons(zoomIn = zoomIn, zoomFactor = factor)
)
Resolved(range)
}
@Composable
private fun Resolved(range: PantrixTimeRange) {
val read = range.resolve()
PantrixText(
text = read?.let { "${moment(it.start)} – ${moment(it.endInclusive)}" } ?: "The end comes before the start",
style = PantrixTheme.typography.footnote,
color = PantrixTheme.colors.textSecondary
)
}
private val LAST_HOUR = last(1, PantrixTimeUnit.Hours)
private const val HALF = 0.5
private const val QUARTER = 0.25
@Composable
private fun moment(at: Instant): String {
val strings = PantrixTheme.strings
val day = PantrixDate.of(at)
val time = PantrixTime.of(at)
return "${strings.date(day.day, day.month, day.year)} ${strings.clock(time.hour, time.minute, time.second)}"
}
private fun last(
amount: Int,
unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)
The quick menu's own panels and order
A panel of the app's own in the quick menu (customQuickPanels), after the ranges used lately by default. "My own order" builds the menu with quickMenu: the saved ranges first, then the quick select, and no ranges offered by name. The menu's parts are in ElasticQuickMenuScope: quickMenu is called in that scope.
var range by remember { mutableStateOf(LAST_QUARTER) }
var own by remember { mutableStateOf(false) }
val saved = remember {
listOf(
"Night shift" to PantrixTimeRange(
PantrixTimePoint.Relative(-1, PantrixTimeUnit.Days, roundTo = PantrixTimeUnit.Days),
PantrixTimePoint.Relative(-16, PantrixTimeUnit.Hours)
),
"Since the last deploy" to last(3, PantrixTimeUnit.Hours)
)
}
val panel = ElasticQuickPanel("Saved ranges") {
Column {
saved.forEachIndexed { index, (name, kept) ->
PantrixLink(text = name, onClick = { range = kept }, tagKey = "saved_$index")
}
}
}
val ownMenu: @Composable ElasticQuickMenuScope.() -> Unit = {
CustomPanels()
QuickSelect()
}
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
PantrixSegmentButton(
options = listOf(
SegmentOption(false, "Elastic UI's order", tagKey = "elastic"),
SegmentOption(true, "My own order", tagKey = "own")
),
selected = own,
onSelect = { own = it },
fullWidth = false,
tagKey = "order"
)
PantrixElasticDatePicker(
range = range,
onRangeChange = { range = it },
tagKey = "quick_panels",
customQuickPanels = listOf(panel),
quickMenu = if (own) ownMenu else null
)
Resolved(range)
}
@Composable
private fun Resolved(range: PantrixTimeRange) {
val read = range.resolve()
PantrixText(
text = read?.let { "${moment(it.start)} – ${moment(it.endInclusive)}" } ?: "The end comes before the start",
style = PantrixTheme.typography.footnote,
color = PantrixTheme.colors.textSecondary
)
}
private fun last(
amount: Int,
unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)
private val LAST_QUARTER = last(15, PantrixTimeUnit.Minutes)
@Composable
private fun moment(at: Instant): String {
val strings = PantrixTheme.strings
val day = PantrixDate.of(at)
val time = PantrixTime.of(at)
return "${strings.date(day.day, day.month, day.year)} ${strings.clock(time.hour, time.minute, time.second)}"
}
Time zone
The moments are read and written in the chosen time zone's difference (utcOffset); the ends' panels name the zone with its difference from UTC (timeZoneName). The name only names, and changes nothing.
var range by remember { mutableStateOf(SINCE_YESTERDAY) }
var zone by remember { mutableStateOf(ZONES.first()) }
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
PantrixSegmentButton(
options = ZONES.mapIndexed { index, it -> SegmentOption(it, it.first, tagKey = "zone_$index") },
selected = zone,
onSelect = { zone = it },
fullWidth = false,
tagKey = "zone"
)
PantrixElasticDatePicker(
range = range,
onRangeChange = { range = it },
tagKey = "time_zone",
timeZoneName = zone.first,
utcOffset = { zone.second }
)
}
private val SINCE_YESTERDAY = PantrixTimeRange(
PantrixTimePoint.Relative(-1, PantrixTimeUnit.Days, roundTo = PantrixTimeUnit.Days),
PantrixTimePoint.Now
)
private val ZONES = listOf("Europe/Istanbul" to 3.hours, "Asia/Tokyo" to 9.hours, "UTC" to Duration.ZERO)
Disabled, with its own words
A picker that takes no choice can show its own words in the place of its range (disabledText), as Elastic UI's isDisabled.display does: here "All time".
var words by remember { mutableStateOf(true) }
Column(verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
PantrixSwitchButton(checked = words, onCheckedChange = { words = it }, label = "Its own words", tagKey = "words")
PantrixElasticDatePicker(
range = LAST_QUARTER,
onRangeChange = {},
enabled = false,
tagKey = "disabled_text",
disabledText = if (words) "All time" else null
)
}
private val LAST_QUARTER = last(15, PantrixTimeUnit.Minutes)
private fun last(
amount: Int,
unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)
The KQL pattern
Elastic UI's KQL pattern: the picker beside a search field. While the search field has the focus the picker shrinks to the quick menu's button (layout = QuickSelectOnly) and the room is the query's; when the focus leaves, the range comes back.
var range by remember { mutableStateOf(LAST_QUARTER) }
var query by remember { mutableStateOf("") }
var searching by remember { mutableStateOf(false) }
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm),
verticalAlignment = Alignment.CenterVertically
) {
PantrixTextField(
value = query,
onValueChange = { query = it },
modifier = Modifier.weight(1f).onFocusChanged { searching = it.hasFocus },
placeholder = "Search with KQL",
showCharacterCount = false,
contentDescription = "Search query",
tagKey = "query"
)
PantrixElasticDatePicker(
range = range,
onRangeChange = { range = it },
modifier = if (searching) Modifier else Modifier.weight(PICKER_SHARE),
updateButton = ElasticDatePickerUpdateButton.Icon,
tagKey = "query_range",
layout = if (searching) ElasticDatePickerLayout.QuickSelectOnly else ElasticDatePickerLayout.Full
)
}
private val LAST_QUARTER = last(15, PantrixTimeUnit.Minutes)
private const val PICKER_SHARE = 1.4f
private fun last(
amount: Int,
unit: PantrixTimeUnit
): PantrixTimeRange = PantrixTimeRange(PantrixTimePoint.Relative(-amount, unit), PantrixTimePoint.Now)
API
range
A range of time that can move with the clock, as a dashboard is filtered by: "the last 15 minutes" is still the last 15 minutes at the next refresh. Elastic UI's EuiSuperDatePicker, built again on Pantrix.
| Parameter | Type | Default | What it is |
|---|---|---|---|
range | PantrixTimeRange | the range on show. Its ends can be relative to now. | |
onRangeChange | (PantrixTimeRange) -> Unit | handed the range the reader chose from the quick menu or updated after an edit, or the same range on a refresh without onRefresh. | |
modifier | Modifier | Modifier | the picker's modifier. The picker fills the width it is given, up to the style's maxWidth. |
commonlyUsed | List<PantrixQuickRange> | PantrixQuickRange.commonlyUsed(PantrixTheme.strings) | the ranges the quick menu offers by name: by default Elastic UI's eight, in the theme's language. A range on show that is one of these is called by its name. |
recentlyUsed | PantrixRecentRanges? | null | the ranges the reader chose lately, which the quick menu lists and each range the reader applies is recorded in; null for none. rememberRecentRanges() keeps them across a configuration change. |
updateButton | ElasticDatePickerUpdateButton | ElasticDatePickerUpdateButton.Text | the button beside the field: its words and icon, its icon alone, or none, every edit then handed over at once. |
presentation | DatePickerPresentation | DatePickerPresentation.Adaptive | what the panels open in: by default a sheet in a narrow window and a popover beside the part pressed in a wider one, chosen as a panel opens; or always a popover, a sheet or a dialog. The same on every platform. |
loading | Boolean | false | whether what the range filters loads: the button beside the field spins and takes no press. |
enabled | Boolean | true | whether the picker takes a choice at all. One that takes none is drawn faint and opens nothing. |
minDate | PantrixDate? | null | the first day an end's calendar offers, or null for none so early. |
maxDate | PantrixDate? | null | the last day it offers, or null for none so late. |
onRefresh | (suspend () -> Unit)? | null | what Refresh does where no edit waits, the button spinning while it runs; null to hand the same range to onRangeChange. |
roundRelativeUnits | Boolean | true | whether an end that is an amount from now is put in the unit that reads best, "~ 2 hours ago" for 90 minutes, or keeps its own. |
firstDayOfWeek | Int? | null | the day the calendar's weeks start on and a week is rounded to, as ISO 8601 numbers the days: 1 is Monday. Null is the language's, PantrixLocale.firstDayOfWeek. |
is24Hour | Boolean | true | whether a moment is written and chosen by a clock of twenty-four hours, 22:00, or of twelve, 10:00 PM. |
style | ElasticDatePickerStyle? | null | this call's own style of the field: over every enclosing one and under the density. |
colors | ElasticDatePickerColors? | null | the field's own colours: the ones it sets, with the theme's for the rest. |
tagKey | String? | null | the picker's automation key: it is ElasticDatePicker_<tagKey>, its parts .Button_quick_menu, .Button_name, .Button_start, .Button_end, .Button_auto_refresh, the time window buttons .Button_window_previous, .Button_window_zoom_in, .Button_window_zoom_out and .Button_window_next, and the button beside the field Button_<tagKey>_update. |
autoRefresh | PantrixAutoRefresh? | null | how often the range is read again by itself, or null for never. While it is not paused the field ends in how often, written short, and Refresh is done at each turn: onRefresh, waited for before the next turn is counted, or the same range handed to onRangeChange. An onRefresh that fails pauses it, and says so through onAutoRefreshChange. |
onAutoRefreshChange | ((PantrixAutoRefresh) -> Unit)? | null | handed the refresh by itself as the reader sets it in the quick menu, where it has a part of its own; null for no such part. |
minAutoRefresh | Duration | Duration.ZERO | the shortest refresh by itself the reader can set: a shorter one draws its amount as wrong. |
layout | ElasticDatePickerLayout | ElasticDatePickerLayout.Full | what the field shows: the range, the quick menu's button alone, or the refresh by itself alone. |
timeWindowButtons | ElasticTimeWindowButtons? | null | Elastic UI's buttons beside the field that move the range by its length and zoom it, or null for none. |
customQuickPanels | List<ElasticQuickPanel> | emptyList() | panels of the caller's own in the quick menu, after the ranges used lately. |
quickMenu | (@Composable ElasticQuickMenuScope.() -> Unit)? | null | the quick menu's parts in an order of the caller's own, some left out or panels of its own put in among them; null for Elastic UI's order. |
timeZoneName | String? | null | the time zone the moments are in, named in the ends' panels with its difference from UTC: "Time zone: Europe/Istanbul (UTC+03:00)". It names, and changes nothing; null for no such line. |
utcOffset | ((Instant) -> Duration)? | null | the difference from UTC the moments are read and written in at a moment; null for the screen's clock's (LocalPantrixClock), the device's unless a test or a preview gives another. |
disabledText | String? | null | what a picker that takes no choice shows in the place of its range; null for the range. |
ElasticDatePickerStyle
PantrixElasticDatePicker's field: how tall it is, its corners, the room inside it, the gap between it and the button beside it, and how wide it gets.
| Parameter | Type | Default | What it is |
|---|---|---|---|
height | Dp | Dp.Unspecified | How tall the field is. |
shape | ShapeSpec | ShapeSpec.Unspecified | The field's corners. |
padding | Dp | Dp.Unspecified | The room between the field's edge and what is in it, at either end. |
gap | Dp | Dp.Unspecified | Between the field and the button beside it, and between the parts in the field. |
maxWidth | Dp | Dp.Unspecified | The widest the field and its button get; Dp.Infinity for as wide as they are given. |
ElasticDatePickerColors
What a PantrixElasticDatePicker's field is painted with, in place of the theme's colours: a colour that is set is used, and one that is left out stays the theme's. What the field opens is painted with the colours of a popover, a sheet, a calendar and tabs.
| Parameter | Type | Default | What it is |
|---|---|---|---|
container | Color | Color.Unspecified | The field's ground. The theme's surface, as a text field's. |
border | Color | Color.Unspecified | The field's edge, and the line between the quick menu's button and the range. The theme's border. |
content | Color | Color.Unspecified | The words of the range and of its ends. The theme's textPrimary. |
icon | Color | Color.Unspecified | The field's icons: the quick menu's calendar and the arrow between the ends. The theme's textSecondary. |
active | Color | Color.Unspecified | The edge drawn round the end whose panel is open. The primary role's color. |
pendingContainer | Color | Color.Unspecified | The field's ground while an edit waits for Update. The success role's container. |
pendingContent | Color | Color.Unspecified | The words, the arrow and the mark on it. The success role's onContainer. |
invalidContainer | Color | Color.Unspecified | The field's ground while its end comes before its start. The danger role's container. |
invalidContent | Color | Color.Unspecified | The words, the arrow and the mark on it. The danger role's onContainer. |
disabled | Color | Color.Unspecified | The words and icons of a field that takes no choice. The theme's textDisabled. |
Accessibility
- Screen reader. Each end is a button by its name and its words ("Start date, ~ 15 minutes ago"); an edit that waits is announced politely.