Forms

TimezonePicker

A drop-down of time zones grouped by region, each with its UTC offset for today, sending the IANA name with a form.

#forms#select#picker#time#no-js

Demo

Preview

Offsets are for today, daylight saving included.

Meetings are set in Asia/Tokyo.

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/timezone_picker.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/timezone_picker.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/timezone_picker.qwe
component TimezonePickerDemoLive() {    state zone = "Asia/Tokyo"    <div.demo-stack>        <TimezonePicker name="meeting_tz" label="Meeting time zone" value={zone} field="timezone-picker-demo-live"                        zones={["UTC", "Europe/London", "Europe/Berlin", "America/New_York", "America/Los_Angeles", "Asia/Tokyo", "Australia/Sydney"]}                        on_change={(z) => { zone = z }}/>        <p.demo-note>Meetings are set in {zone}.</p>    </div>}view TimezonePickerDemo() =    <div.demo-stack.demo-narrow>        <TimezonePicker name="tz" label="Time zone" value="Europe/Lisbon" field="timezone-picker-demo-all"                        hint="Offsets are for today, daylight saving included." required={true}/>        <TimezonePickerDemoLive/>        <div.demo-row>            <TimezonePicker name="tz_small" label="Small" size="sm" placeholder="Pick a zone" field="timezone-picker-demo-small"                            zones={["Europe/Paris", "Asia/Kolkata", "Pacific/Auckland"]}/>            <TimezonePicker name="tz_locked" label="Disabled" size="sm" value="UTC" disabled={true} zones={["UTC"]}/>        </div>    </div>

Source

ui/timezone_picker.qwe
// TimezonePicker: a drop-down of time zones grouped by region, each with its UTC// offset ("(UTC+01:00) Paris"). It is the browser's own <select>, so a plain <form>// sends the IANA name ("Europe/Paris") as `name`, and it works with the keyboard.////   <TimezonePicker name="tz" label="Time zone" value="Europe/Lisbon"/>//   <TimezonePicker name="tz" label="Time zone" zones={["UTC", "Europe/London", "America/New_York"]}/>//// value: the zone chosen to start with (an IANA name). zones: the names to offer,// in place of the built-in list of about a hundred; a region is the part before// the slash. at: the date-time the offsets are worked out for (now by default), so// daylight saving is right. placeholder: a first line that cannot be chosen.// hint, error, required, disabled, field (the id, default "timezone-picker-" +// name) and size ("sm" or "md") as in Select.// on_change(zone): inside one of your components, given the IANA name chosen.import ui/theme.qwe use ui_clslet timezone_picker_css = css`.ui-timezone-picker { display: grid; gap: 6px; min-width: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-timezone-picker-label { font-size: 13px; font-weight: 500; color: var(--ui-fg); }.ui-timezone-picker-req { margin-left: 2px; color: var(--ui-danger); }.ui-timezone-picker-box { position: relative; display: block; }.ui-timezone-picker-box::after {  content: ""; position: absolute; right: 14px; top: 50%; width: 7px; height: 7px; margin-top: -6px;  border: solid var(--ui-muted); border-width: 0 1.5px 1.5px 0; transform: rotate(45deg); pointer-events: none;}.ui-timezone-picker select {  appearance: none; display: block; width: 100%; height: 38px; padding: 0 36px 0 12px;  background: var(--ui-surface); color: var(--ui-fg); font: 14px var(--ui-font); font-variant-numeric: tabular-nums;  border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); cursor: pointer;  text-overflow: ellipsis; transition: border-color .15s, box-shadow .15s;}.ui-timezone-picker select:hover { border-color: var(--ui-strong); }.ui-timezone-picker select:focus-visible { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-timezone-picker select:has(option[value=""]:checked) { color: var(--ui-muted); }.ui-timezone-picker option { color: var(--ui-fg); background: var(--ui-surface); }.ui-timezone-picker-sm select { height: 30px; padding: 0 30px 0 10px; font-size: 13px; }.ui-timezone-picker-sm .ui-timezone-picker-box::after { right: 11px; width: 6px; height: 6px; margin-top: -5px; }.ui-timezone-picker-invalid select, .ui-timezone-picker-invalid select:hover { border-color: var(--ui-danger); }.ui-timezone-picker-hint, .ui-timezone-picker-error { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-timezone-picker-error { color: var(--ui-danger); }.ui-timezone-picker-disabled { opacity: .55; }.ui-timezone-picker-disabled select { cursor: not-allowed; }@media (prefers-reduced-motion: reduce) { .ui-timezone-picker select { transition: none; } }`let timezone_picker_zones = [    "UTC",    "Africa/Abidjan", "Africa/Accra", "Africa/Addis_Ababa", "Africa/Algiers", "Africa/Cairo", "Africa/Casablanca",    "Africa/Dar_es_Salaam", "Africa/Johannesburg", "Africa/Khartoum", "Africa/Kinshasa", "Africa/Lagos", "Africa/Nairobi",    "Africa/Tunis",    "America/Anchorage", "America/Argentina/Buenos_Aires", "America/Asuncion", "America/Bogota", "America/Caracas",    "America/Chicago", "America/Denver", "America/Guatemala", "America/Halifax", "America/Havana", "America/La_Paz",    "America/Lima", "America/Los_Angeles", "America/Mexico_City", "America/Montevideo", "America/New_York",    "America/Panama", "America/Phoenix", "America/Santiago", "America/Sao_Paulo", "America/St_Johns", "America/Toronto",    "America/Vancouver",    "Asia/Almaty", "Asia/Baghdad", "Asia/Baku", "Asia/Bangkok", "Asia/Beirut", "Asia/Colombo", "Asia/Dhaka", "Asia/Dubai",    "Asia/Hong_Kong", "Asia/Jakarta", "Asia/Jerusalem", "Asia/Kabul", "Asia/Karachi", "Asia/Kathmandu", "Asia/Kolkata",    "Asia/Kuala_Lumpur", "Asia/Manila", "Asia/Riyadh", "Asia/Seoul", "Asia/Shanghai", "Asia/Singapore", "Asia/Taipei",    "Asia/Tashkent", "Asia/Tbilisi", "Asia/Tehran", "Asia/Tokyo", "Asia/Yangon", "Asia/Yerevan",    "Atlantic/Azores", "Atlantic/Cape_Verde", "Atlantic/Reykjavik",    "Australia/Adelaide", "Australia/Brisbane", "Australia/Darwin", "Australia/Hobart", "Australia/Melbourne",    "Australia/Perth", "Australia/Sydney",    "Europe/Amsterdam", "Europe/Athens", "Europe/Belgrade", "Europe/Berlin", "Europe/Brussels", "Europe/Bucharest",    "Europe/Budapest", "Europe/Copenhagen", "Europe/Dublin", "Europe/Helsinki", "Europe/Istanbul", "Europe/Kyiv",    "Europe/Lisbon", "Europe/London", "Europe/Madrid", "Europe/Moscow", "Europe/Oslo", "Europe/Paris", "Europe/Prague",    "Europe/Rome", "Europe/Stockholm", "Europe/Vienna", "Europe/Warsaw", "Europe/Zurich",    "Indian/Maldives", "Indian/Mauritius",    "Pacific/Auckland", "Pacific/Fiji", "Pacific/Guam", "Pacific/Honolulu", "Pacific/Port_Moresby", "Pacific/Tongatapu",]// "+01:00" for a zone at the instant `at` ("+00:00" for UTC).fn timezone_picker_offset(zone: str, at: str) -> str {    let t = date.to_zone(at, zone)    if is_err(t) || t == null { return "+00:00" }    let tail = slice(t, len(t) - 6, len(t))    ends_with(t, "Z") ? "+00:00" : tail}// "Buenos Aires" from "America/Argentina/Buenos_Aires".fn timezone_picker_city(zone: str) -> str = replace(last(split(zone, "/")), "_", " ")// Minutes from UTC, from "+05:30", for sorting.fn timezone_picker_minutes(offset: str) -> int {    let sign = starts_with(offset, "-") ? -1 : 1    sign * ((int(slice(offset, 1, 3)) ?? 0) * 60 + (int(slice(offset, 4, 6)) ?? 0))}view TimezonePicker(name: str? = null, label: str? = null, value: str? = null, zones: list? = null, at: str? = null,                    placeholder: str? = null, hint: str? = null, error: str? = null, required: bool = false,                    disabled: bool = false, field: str? = null, size: str = "md", on_change = null, class: str? = null) {    style global timezone_picker_css    let id = field ?? `timezone-picker-${name ?? "zone"}`    let described = ui_cls([hint != null && `${id}-hint`, error != null && `${id}-error`])    let when = at ?? date.now()    let rows = (zones ?? timezone_picker_zones) | map((z) => {        let off = timezone_picker_offset(z, when)        { zone: z, region: contains(z, "/") ? first(split(z, "/")) : "Other", city: timezone_picker_city(z),          offset: off, minutes: timezone_picker_minutes(off) }    })    let regions = rows | map((r) => r.region) | unique    let opt = (r) => <option value={r.zone} selected={r.zone == value}>{`(UTC${r.offset}) ${r.city}`}</option>    <div class={ui_cls(["ui-timezone-picker", size == "sm" && "ui-timezone-picker-sm", error != null && "ui-timezone-picker-invalid", disabled && "ui-timezone-picker-disabled", class])}>        {label != null && <label.ui-timezone-picker-label for={id}>            {label}{required && <span.ui-timezone-picker-req aria-hidden="true">*</span>}        </label>}        <span.ui-timezone-picker-box>            <select id={id} name={name} required={required} disabled={disabled}                    aria-invalid={error != null ? "true" : null} aria-describedby={described != "" ? described : null}                    @change={(e) => { on_change && on_change(e.value) }}>                {placeholder != null && <option value="" disabled selected={value == null}>{placeholder}</option>}                {for g in regions => {                    let mine = rows | filter((r) => r.region == g) | sort_by((r) => r.city) | sort_by((r) => r.minutes)                    g == "Other" ? (mine | map(opt)) : <optgroup label={g}>{for r in mine => opt(r)}</optgroup>                }}            </select>        </span>        {hint != null && <p.ui-timezone-picker-hint id={`${id}-hint`}>{hint}</p>}        {error != null && <p.ui-timezone-picker-error id={`${id}-error`} role="alert">{error}</p>}    </div>}
Thread
55Useful? Like it, or add to its thread below.

Thread 0 posts

On TimezonePicker: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.