diff --git a/packages/bbui/src/Form/Core/DatePicker/Calendar.svelte b/packages/bbui/src/Form/Core/DatePicker/Calendar.svelte
index 83692ff898..bc06530044 100644
--- a/packages/bbui/src/Form/Core/DatePicker/Calendar.svelte
+++ b/packages/bbui/src/Form/Core/DatePicker/Calendar.svelte
@@ -3,10 +3,11 @@
import Select from "../../Select.svelte"
import dayjs from "dayjs"
import NumberInput from "./NumberInput.svelte"
+ import { createEventDispatcher } from "svelte"
export let value
- export let onChange
+ const dispatch = createEventDispatcher()
const DaysOfWeek = [
"Monday",
"Tuesday",
@@ -58,7 +59,10 @@
const handleDateChange = date => {
const base = value || now
- onChange(base.year(date.year()).month(date.month()).date(date.date()))
+ dispatch(
+ "change",
+ base.year(date.year()).month(date.month()).date(date.date())
+ )
}
export const setDate = date => {
@@ -224,6 +228,9 @@
.spectrum-Calendar-date.is-selected {
color: white;
}
+ .spectrum-Calendar-dayOfWeek {
+ color: var(--spectrum-global-color-gray-600);
+ }
/* Style select */
.month-selector :global(.spectrum-Picker) {
diff --git a/packages/bbui/src/Form/Core/DatePicker/DatePicker.svelte b/packages/bbui/src/Form/Core/DatePicker/DatePicker.svelte
index a29746ee75..4587195865 100644
--- a/packages/bbui/src/Form/Core/DatePicker/DatePicker.svelte
+++ b/packages/bbui/src/Form/Core/DatePicker/DatePicker.svelte
@@ -3,13 +3,10 @@
import "@spectrum-css/inputgroup/dist/index-vars.css"
import "@spectrum-css/textfield/dist/index-vars.css"
import Popover from "../../../Popover/Popover.svelte"
- import dayjs from "dayjs"
- import { createEventDispatcher, onMount } from "svelte"
- import TimePicker from "./TimePicker.svelte"
- import Calendar from "./Calendar.svelte"
+ import { onMount } from "svelte"
import DateInput from "./DateInput.svelte"
- import ActionButton from "../../../ActionButton/ActionButton.svelte"
import { parseDate } from "../../../helpers"
+ import DatePickerPopoverContents from "./DatePickerPopoverContents.svelte"
export let id = null
export let disabled = false
@@ -25,74 +22,18 @@
export let api = null
export let align = "left"
- const dispatch = createEventDispatcher()
-
let isOpen = false
let anchor
let popover
- let calendar
- $: parsedValue = parseDate(value, { timeOnly, dateOnly: !enableTime })
- $: showCalendar = !timeOnly
- $: showTime = enableTime || timeOnly
-
- const clearDateOnBackspace = event => {
- // Ignore if we're typing a value
- if (document.activeElement?.tagName.toLowerCase() === "input") {
- return
- }
- if (["Backspace", "Clear", "Delete"].includes(event.key)) {
- handleChange(null)
- popover?.hide()
- }
- }
+ $: parsedValue = parseDate(value, { timeOnly, enableTime })
const onOpen = () => {
isOpen = true
- if (useKeyboardShortcuts) {
- document.addEventListener("keyup", clearDateOnBackspace)
- }
}
const onClose = () => {
isOpen = false
- if (useKeyboardShortcuts) {
- document.removeEventListener("keyup", clearDateOnBackspace)
- }
- }
-
- const handleChange = date => {
- if (!date) {
- dispatch("change", null)
- return
- }
- let newValue = date.toISOString()
-
- // Time only fields always ignore timezones, otherwise they make no sense.
- // For non-timezone-aware fields, create an ISO 8601 timestamp of the exact
- // time picked, without timezone
- const offsetForTimezone = (enableTime && ignoreTimezones) || timeOnly
- if (offsetForTimezone) {
- const offset = new Date().getTimezoneOffset() * 60000
- newValue = new Date(date.valueOf() - offset).toISOString().slice(0, -1)
- }
-
- // For date-only fields, construct a manual timestamp string without a time
- // or time zone
- else if (!enableTime) {
- const year = date.year()
- const month = `${date.month() + 1}`.padStart(2, "0")
- const day = `${date.date()}`.padStart(2, "0")
- newValue = `${year}-${month}-${day}T00:00:00.000`
- }
-
- dispatch("change", newValue)
- }
-
- const setToNow = () => {
- const now = dayjs()
- calendar?.setDate(now)
- handleChange(now)
}
onMount(() => {
@@ -130,54 +71,13 @@
{align}
>
{#if isOpen}
-
- {#if showCalendar}
-
- {/if}
-
-
+
{/if}
-
-
diff --git a/packages/bbui/src/Form/Core/DatePicker/DatePickerPopoverContents.svelte b/packages/bbui/src/Form/Core/DatePicker/DatePickerPopoverContents.svelte
new file mode 100644
index 0000000000..f1bb809b29
--- /dev/null
+++ b/packages/bbui/src/Form/Core/DatePicker/DatePickerPopoverContents.svelte
@@ -0,0 +1,102 @@
+
+
+
+ {#if showCalendar}
+ handleChange(e.detail)}
+ bind:this={calendar}
+ />
+ {/if}
+
+
+
+
diff --git a/packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte b/packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte
index 91dd95ff5f..dc4886d28d 100644
--- a/packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte
+++ b/packages/bbui/src/Form/Core/DatePicker/NumberInput.svelte
@@ -33,7 +33,7 @@
font-weight: bold;
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
- box-sizing: content-box;
+ box-sizing: content-box !important;
}
input:focus,
input:hover {
diff --git a/packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte b/packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte
index adf2a5e87a..047e5a4f08 100644
--- a/packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte
+++ b/packages/bbui/src/Form/Core/DatePicker/TimePicker.svelte
@@ -2,18 +2,20 @@
import { cleanInput } from "./utils"
import dayjs from "dayjs"
import NumberInput from "./NumberInput.svelte"
+ import { createEventDispatcher } from "svelte"
export let value
- export let onChange
+
+ const dispatch = createEventDispatcher()
$: displayValue = value || dayjs()
const handleHourChange = e => {
- onChange(displayValue.hour(parseInt(e.target.value)))
+ dispatch("change", displayValue.hour(parseInt(e.target.value)))
}
const handleMinuteChange = e => {
- onChange(displayValue.minute(parseInt(e.target.value)))
+ dispatch("change", displayValue.minute(parseInt(e.target.value)))
}
const cleanHour = cleanInput({ max: 23, pad: 2, fallback: "00" })
@@ -51,7 +53,7 @@
.time-picker span {
font-weight: bold;
font-size: 18px;
- z-index: -1;
+ z-index: 0;
margin-bottom: 1px;
}
diff --git a/packages/bbui/src/Form/Core/index.js b/packages/bbui/src/Form/Core/index.js
index 533a1956c5..7117b90081 100644
--- a/packages/bbui/src/Form/Core/index.js
+++ b/packages/bbui/src/Form/Core/index.js
@@ -9,6 +9,7 @@ export { default as CoreCombobox } from "./Combobox.svelte"
export { default as CoreSwitch } from "./Switch.svelte"
export { default as CoreSearch } from "./Search.svelte"
export { default as CoreDatePicker } from "./DatePicker/DatePicker.svelte"
+export { default as CoreDatePickerPopoverContents } from "./DatePicker/DatePickerPopoverContents.svelte"
export { default as CoreDateRangePicker } from "./DateRangePicker.svelte"
export { default as CoreDropzone } from "./Dropzone.svelte"
export { default as CoreStepper } from "./Stepper.svelte"
diff --git a/packages/bbui/src/helpers.js b/packages/bbui/src/helpers.js
index c98ebad2c7..4448527fea 100644
--- a/packages/bbui/src/helpers.js
+++ b/packages/bbui/src/helpers.js
@@ -117,7 +117,9 @@ export const copyToClipboard = value => {
})
}
-export const parseDate = (value, { dateOnly } = {}) => {
+// Parsed a date value. This is usually an ISO string, but can be a
+// bunch of different formats and shapes depending on schema flags.
+export const parseDate = (value, { enableTime = true }) => {
// If empty then invalid
if (!value) {
return null
@@ -131,7 +133,7 @@ export const parseDate = (value, { dateOnly } = {}) => {
}
// If date only, check for cases where we received a UTC string
- else if (dateOnly && value.endsWith("Z")) {
+ else if (!enableTime && value.endsWith("Z")) {
value = value.split("Z")[0]
}
}
@@ -148,7 +150,42 @@ export const parseDate = (value, { dateOnly } = {}) => {
return dayjs(Math.floor(parsedDate.valueOf() / 1000) * 1000)
}
-export const getDateDisplayValue = (value, { enableTime, timeOnly }) => {
+// Stringifies a dayjs object to create an ISO string that respects the various
+// schema flags
+export const stringifyDate = (
+ value,
+ { enableTime = true, timeOnly = false, ignoreTimezones = false }
+) => {
+ if (!value) {
+ return null
+ }
+
+ // Time only fields always ignore timezones, otherwise they make no sense.
+ // For non-timezone-aware fields, create an ISO 8601 timestamp of the exact
+ // time picked, without timezone
+ const offsetForTimezone = (enableTime && ignoreTimezones) || timeOnly
+ if (offsetForTimezone) {
+ // Ensure we use the correct offset for the date
+ const referenceDate = timeOnly ? new Date() : value.toDate()
+ const offset = referenceDate.getTimezoneOffset() * 60000
+ return new Date(value.valueOf() - offset).toISOString().slice(0, -1)
+ }
+
+ // For date-only fields, construct a manual timestamp string without a time
+ // or time zone
+ else if (!enableTime) {
+ const year = value.year()
+ const month = `${value.month() + 1}`.padStart(2, "0")
+ const day = `${value.date()}`.padStart(2, "0")
+ return `${year}-${month}-${day}T00:00:00.000`
+ }
+}
+
+// Formats a dayjs date according to schema flags
+export const getDateDisplayValue = (
+ value,
+ { enableTime = true, timeOnly = false }
+) => {
if (!value?.isValid()) {
return ""
}
diff --git a/packages/frontend-core/src/components/grid/cells/DateCell.svelte b/packages/frontend-core/src/components/grid/cells/DateCell.svelte
index 019a0db9c2..394b6e1f43 100644
--- a/packages/frontend-core/src/components/grid/cells/DateCell.svelte
+++ b/packages/frontend-core/src/components/grid/cells/DateCell.svelte
@@ -1,6 +1,13 @@
-
+
+
+
{displayValue}
@@ -55,17 +107,14 @@
{/if}
-{#if editable}
-
-
+ onChange(e.detail)}
- enableTime={!dateOnly}
+ {enableTime}
{timeOnly}
- ignoreTimezones={schema.ignoreTimezones}
- bind:api={datePickerAPI}
- on:open={() => (isOpen = true)}
- on:close={() => (isOpen = false)}
+ {ignoreTimezones}
useKeyboardShortcuts={false}
/>
@@ -80,6 +129,10 @@
align-items: center;
flex: 1 1 auto;
gap: var(--cell-spacing);
+ user-select: none;
+ }
+ .container.editable:hover {
+ cursor: pointer;
}
.value {
flex: 1 1 auto;
@@ -92,9 +145,10 @@
}
.picker {
position: absolute;
- opacity: 0;
- }
- .picker :global(.spectrum-Textfield-input) {
- width: 100%;
+ top: 100%;
+ left: -1px;
+ background: var(--grid-background-alt);
+ border: var(--cell-border);
+ border-radius: 2px;
}
diff --git a/packages/frontend-core/src/components/grid/cells/LongFormCell.svelte b/packages/frontend-core/src/components/grid/cells/LongFormCell.svelte
index 0299e66e2f..b3eab24fa1 100644
--- a/packages/frontend-core/src/components/grid/cells/LongFormCell.svelte
+++ b/packages/frontend-core/src/components/grid/cells/LongFormCell.svelte
@@ -103,8 +103,8 @@
position: absolute;
top: 0;
left: 0;
- width: calc(100% + var(--max-cell-render-width-overflow));
- height: calc(var(--row-height) + var(--max-cell-render-height));
+ width: calc(100% + var(--max-cell-render-verflow));
+ height: calc(var(--row-height) + var(--max-cell-render-overflow));
z-index: 1;
border-radius: 2px;
resize: none;
diff --git a/packages/frontend-core/src/components/grid/cells/OptionsCell.svelte b/packages/frontend-core/src/components/grid/cells/OptionsCell.svelte
index ede9bd1cff..158451c930 100644
--- a/packages/frontend-core/src/components/grid/cells/OptionsCell.svelte
+++ b/packages/frontend-core/src/components/grid/cells/OptionsCell.svelte
@@ -23,7 +23,7 @@
$: values = Array.isArray(value) ? value : [value].filter(x => x != null)
$: {
// Close when deselected
- if (!focused) {
+ if (!focused && isOpen) {
close()
}
}
@@ -219,7 +219,7 @@
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
- max-height: var(--max-cell-render-height);
+ max-height: var(--max-cell-render-overflow);
overflow-y: auto;
border: var(--cell-border);
box-shadow: 0 0 20px -4px rgba(0, 0, 0, 0.15);
diff --git a/packages/frontend-core/src/components/grid/cells/RelationshipCell.svelte b/packages/frontend-core/src/components/grid/cells/RelationshipCell.svelte
index bf1fe92ef0..a52dd9d53c 100644
--- a/packages/frontend-core/src/components/grid/cells/RelationshipCell.svelte
+++ b/packages/frontend-core/src/components/grid/cells/RelationshipCell.svelte
@@ -35,7 +35,7 @@
$: lookupMap = buildLookupMap(value, isOpen)
$: debouncedSearch(searchString)
$: {
- if (!focused) {
+ if (!focused && isOpen) {
close()
}
}
@@ -451,7 +451,7 @@
left: 0;
width: 100%;
max-height: calc(
- var(--max-cell-render-height) + var(--row-height) - var(--values-height)
+ var(--max-cell-render-overflow) + var(--row-height) - var(--values-height)
);
background: var(--grid-background-alt);
border: var(--cell-border);
diff --git a/packages/frontend-core/src/components/grid/layout/Grid.svelte b/packages/frontend-core/src/components/grid/layout/Grid.svelte
index b6c686fd62..468f49bdec 100644
--- a/packages/frontend-core/src/components/grid/layout/Grid.svelte
+++ b/packages/frontend-core/src/components/grid/layout/Grid.svelte
@@ -22,8 +22,7 @@
import NewRow from "./NewRow.svelte"
import { createGridWebsocket } from "../lib/websocket"
import {
- MaxCellRenderHeight,
- MaxCellRenderWidthOverflow,
+ MaxCellRenderOverflow,
GutterWidth,
DefaultRowHeight,
} from "../lib/constants"
@@ -78,6 +77,7 @@
contentLines,
gridFocused,
error,
+ focusedCellId,
} = context
// Keep config store up to date with props
@@ -129,7 +129,7 @@
class:quiet
on:mouseenter={() => gridFocused.set(true)}
on:mouseleave={() => gridFocused.set(false)}
- style="--row-height:{$rowHeight}px; --default-row-height:{DefaultRowHeight}px; --gutter-width:{GutterWidth}px; --max-cell-render-height:{MaxCellRenderHeight}px; --max-cell-render-width-overflow:{MaxCellRenderWidthOverflow}px; --content-lines:{$contentLines};"
+ style="--row-height:{$rowHeight}px; --default-row-height:{DefaultRowHeight}px; --gutter-width:{GutterWidth}px; --max-cell-render-overflow:{MaxCellRenderOverflow}px; --content-lines:{$contentLines};"
>
{#if showControls}
diff --git a/packages/frontend-core/src/components/grid/lib/constants.js b/packages/frontend-core/src/components/grid/lib/constants.js
index a6e6723463..37d829873d 100644
--- a/packages/frontend-core/src/components/grid/lib/constants.js
+++ b/packages/frontend-core/src/components/grid/lib/constants.js
@@ -1,5 +1,4 @@
export const Padding = 246
-export const MaxCellRenderHeight = 222
export const ScrollBarSize = 8
export const GutterWidth = 72
export const DefaultColumnWidth = 200
@@ -12,4 +11,4 @@ export const NewRowID = "new"
export const BlankRowID = "blank"
export const RowPageSize = 100
export const FocusedCellMinOffset = 48
-export const MaxCellRenderWidthOverflow = Padding - 3 * ScrollBarSize
+export const MaxCellRenderOverflow = Padding - 3 * ScrollBarSize
diff --git a/packages/frontend-core/src/components/grid/stores/viewport.js b/packages/frontend-core/src/components/grid/stores/viewport.js
index 8df8acd0f4..96a5a954ee 100644
--- a/packages/frontend-core/src/components/grid/stores/viewport.js
+++ b/packages/frontend-core/src/components/grid/stores/viewport.js
@@ -1,7 +1,6 @@
import { derived } from "svelte/store"
import {
- MaxCellRenderHeight,
- MaxCellRenderWidthOverflow,
+ MaxCellRenderOverflow,
MinColumnWidth,
ScrollBarSize,
} from "../lib/constants"
@@ -95,11 +94,11 @@ export const deriveStores = context => {
// Compute the last row index with space to render popovers below it
const minBottom =
- $height - ScrollBarSize * 3 - MaxCellRenderHeight + offset
+ $height - ScrollBarSize * 3 - MaxCellRenderOverflow + offset
const lastIdx = Math.floor(minBottom / $rowHeight)
// Compute the first row index with space to render popovers above it
- const minTop = MaxCellRenderHeight + offset
+ const minTop = MaxCellRenderOverflow + offset
const firstIdx = Math.ceil(minTop / $rowHeight)
// Use the greater of the two indices so that we prefer content below,
@@ -117,7 +116,7 @@ export const deriveStores = context => {
let inversionIdx = $visibleColumns.length
for (let i = $visibleColumns.length - 1; i >= 0; i--, inversionIdx--) {
const rightEdge = $visibleColumns[i].left + $visibleColumns[i].width
- if (rightEdge + MaxCellRenderWidthOverflow <= cutoff) {
+ if (rightEdge + MaxCellRenderOverflow <= cutoff) {
break
}
}