date-picker: mode='modal' + footer (clear/cancel/closeButton)

Replicates the date-range-picker pattern on the single date-picker:

soma:
- DatePickerMode type ('inline' | 'modal') exported.
- Provider gains mode/clearButton/cancelButton/closeButton opts plus
  commit() / cancel() / clear() action helpers; cancel() reverts the
  value snapshot taken via watch() on the OPEN edge of opts.open.
- handleDateSelect now early-returns when mode === 'modal' so modal
  pickers don't auto-close on selection.
- exports.ts surfaces DatePickerProvider + DatePickerMode for demo /
  footer wiring (mirrors date-range-picker's barrel).
- Test fixture extended with the new opts (closeOnDateSelect stays
  true by default; mode='inline', all buttons false).

morfo:
- scope=['soma','sema'], apg=dialog-modal.
- 6 events: open / close-commit / close-cancel / close-dismiss /
  close-dismiss-outside / commit-clear. prewrite stamps data-last-action
  with the causal exit reason so Sema can tint the exit animation.
- Calendar part declares the state machine (open/closed,
  data-last-action, data-starting-style, data-ending-style) and the
  modal-keyboard surface (Escape / Tab / Shift+Tab).

demo:
- New picker-actions.svelte mirrors the range demo: pulls provider via
  context, renders Clear / Cancel / Close conditionally, forces Close in
  modal mode. CSS is local to the file (uses --color-primary-* +
  --color-surface-overlay tokens, no raw colors).
- +page.svelte adds mode radio chip group + 3 footer-button switches and
  passes mode/clearButton/cancelButton/closeButton through to the Eidos
  wrapper (which spreads to soma).

The Eidos wrapper needs no change — it already spreads everything via
...rest, so the new soma opts reach Soma without further wiring.

Verification: 0 type errors, 3/3 soma tests pass, morfo:check PASS,
component:audit 67/67 PASS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 5 months ago
parent 6144837174
commit 9bd9db408d

@ -8,7 +8,7 @@
| Item | Disposición | Notas | | Item | Disposición | Notas |
| --- | --- | --- | | --- | --- | --- |
| Aplicar `mode: 'inline' \| 'modal'` + footer (`clearButton`/`cancelButton`/`closeButton`) a `date-picker` (single) | **implementar** | Mismo patrón que el range. Provider expone `clear/cancel/commit`. Modal fuerza closeButton. | | Aplicar `mode: 'inline' \| 'modal'` + footer (`clearButton`/`cancelButton`/`closeButton`) a `date-picker` (single) | **hecho** 2026-05-21 | Patrón replicado del range. Provider expone `commit/cancel/clear` + snapshot vía `watch(open)`. Morfo expandido a 6 eventos (open/close-commit/close-cancel/close-dismiss/close-dismiss-outside/commit-clear) + scope:`['soma', 'sema']` + APG dialog-modal. Demo expone chips `mode` + 3 switches en footer. Pendiente verificación visual. |
| Propagar el patrón modal+footer a `time-picker`, `time-range-picker`, `color-picker`, `combobox`, `select`, `popover` cuando proceda | **implementar** | Decidir por componente; muchos no necesitan footer. | | Propagar el patrón modal+footer a `time-picker`, `time-range-picker`, `color-picker`, `combobox`, `select`, `popover` cuando proceda | **implementar** | Decidir por componente; muchos no necesitan footer. |
| `MonthRangePicker` (Chakra-style) | **implementar** | Componente nuevo. Segments month/year + grid de meses, sin calendario de días. | | `MonthRangePicker` (Chakra-style) | **implementar** | Componente nuevo. Segments month/year + grid de meses, sin calendario de días. |
| `YearRangePicker` | **implementar** | Componente nuevo. Segments year + grid de años. | | `YearRangePicker` | **implementar** | Componente nuevo. Segments year + grid de años. |

@ -1,16 +1,132 @@
import type { Morfo } from '../types'; import type { Morfo } from '../types';
import { v } from '../types'; import { v } from '../types';
/**
* DatePicker — composite popover containing a DateField (segmented input)
* + Calendar. The morfo declares the picker's own surface (trigger +
* calendar shell + dismissal/commit events); cell-level events live on
* the `calendar` morfo.
*/
export const datePickerMorfo = { export const datePickerMorfo = {
name: 'DatePicker', name: 'DatePicker',
kebab: 'date-picker', kebab: 'date-picker',
scope: ['soma'], scope: ['soma', 'sema'],
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/',
texts: { texts: {
label: '#?components.date-picker.label|Date Picker', label: '#?components.date-picker.label|Date Picker',
trigger: '#?components.date-picker.trigger|Open calendar', trigger: '#?components.date-picker.trigger|Open calendar',
calendar: '#?components.date-picker.calendar|Date calendar' calendar: '#?components.date-picker.calendar|Date calendar'
}, },
events: [
{
name: 'open',
semantic: {
family: 'emerge',
verb: 'open',
target: v.partRef('calendar'),
sequence: 'pre',
intent: {
fromProp: 'intent',
default: 'neutral',
supported: ['neutral', 'affirm', 'risk']
}
},
commits: {
part: v.partRef('calendar'),
attr: 'data-state',
value: 'open'
}
},
{
// The user committed a date and the popover closed.
// `fulfill` because the picker's job — pick a date — is done.
name: 'close-commit',
semantic: {
family: 'commit',
verb: 'save',
target: v.partRef('calendar'),
sequence: 'post',
intent: 'fulfill'
},
regime: 'lock',
prewrite: [{ part: v.partRef('calendar'), attr: 'data-last-action', value: 'committed' }],
commits: {
part: v.partRef('calendar'),
attr: 'data-state',
value: 'closed'
}
},
{
// Explicit cancel from a Save/Cancel button (modal mode).
name: 'close-cancel',
semantic: {
family: 'emerge',
verb: 'close',
target: v.partRef('calendar'),
sequence: 'pre'
},
regime: 'lock',
prewrite: [{ part: v.partRef('calendar'), attr: 'data-last-action', value: 'cancelled' }],
commits: {
part: v.partRef('calendar'),
attr: 'data-state',
value: 'closed'
}
},
{
// Escape key.
name: 'close-dismiss',
semantic: {
family: 'emerge',
verb: 'dismiss',
target: v.partRef('calendar'),
sequence: 'pre'
},
regime: 'lock',
prewrite: [{ part: v.partRef('calendar'), attr: 'data-last-action', value: 'dismissed' }],
commits: {
part: v.partRef('calendar'),
attr: 'data-state',
value: 'closed'
}
},
{
// Click outside the popover.
name: 'close-dismiss-outside',
semantic: {
family: 'emerge',
verb: 'dismiss',
target: v.partRef('calendar'),
sequence: 'pre'
},
regime: 'lock',
prewrite: [
{
part: v.partRef('calendar'),
attr: 'data-last-action',
value: 'dismissed-outside'
}
],
commits: {
part: v.partRef('calendar'),
attr: 'data-state',
value: 'closed'
}
},
{
// User cleared the date (explicit clear button or backspace).
name: 'commit-clear',
semantic: {
family: 'commit',
verb: 'reset',
target: v.partRef('calendar'),
sequence: 'post',
intent: 'neutral'
}
}
],
focus: { focus: {
initial: 'first-focusable', initial: 'first-focusable',
trap: true, trap: true,
@ -28,11 +144,11 @@ export const datePickerMorfo = {
optional: false, optional: false,
states: ['open', 'closed'], states: ['open', 'closed'],
data: [ data: [
{ attr: 'data-state', values: ['open', 'closed'] }, { attr: 'data-state', values: ['open', 'closed'], value: v.stateRef('open') },
{ attr: 'data-disabled', severity: 'optional' }, { attr: 'data-disabled', value: v.propRef('disabled'), severity: 'optional' },
{ attr: 'data-readonly', severity: 'optional' }, { attr: 'data-readonly', value: v.propRef('readonly'), severity: 'optional' },
{ attr: 'data-required', severity: 'optional' }, { attr: 'data-required', value: v.propRef('required'), severity: 'optional' },
{ attr: 'data-invalid', severity: 'optional' } { attr: 'data-invalid', value: v.propRef('invalid'), severity: 'optional' }
], ],
aria: [ aria: [
{ attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' }, { attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' },
@ -49,7 +165,7 @@ export const datePickerMorfo = {
role: 'button', role: 'button',
optional: false, optional: false,
states: ['open', 'closed'], states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }], data: [{ attr: 'data-state', values: ['open', 'closed'], value: v.stateRef('open') }],
aria: [ aria: [
{ attr: 'type', value: v.literal('button') }, { attr: 'type', value: v.literal('button') },
{ attr: 'aria-haspopup', value: v.literal('dialog') }, { attr: 'aria-haspopup', value: v.literal('dialog') },
@ -65,17 +181,56 @@ export const datePickerMorfo = {
value: v.translationRef('#?components.date-picker.trigger|Open calendar'), value: v.translationRef('#?components.date-picker.trigger|Open calendar'),
severity: 'recommended' severity: 'recommended'
} }
],
keyboard: [
{ key: 'Enter', action: 'open' },
{ key: ' ', action: 'open' }
] ]
}, },
{ {
name: 'Calendar', name: 'Calendar',
kebab: 'calendar', kebab: 'calendar',
archetype: 'content',
kind: 'public', kind: 'public',
defaultElement: 'div', defaultElement: 'div',
role: 'dialog', role: 'dialog',
optional: false, optional: false,
data: [], states: ['open', 'closed'],
aria: [] data: [
{ attr: 'data-state', values: ['open', 'closed'], value: v.stateRef('open') },
{
/**
* Causal exit reason — mirror of dialog's `data-last-action`.
* Updated by `prewrite` BEFORE `data-state` flips to `closed`,
* so Sema can tint the exit animation per-action.
*/
attr: 'data-last-action',
values: ['committed', 'cancelled', 'dismissed', 'dismissed-outside'],
severity: 'optional'
},
{
attr: 'data-starting-style',
severity: 'optional',
condition: { when: 'state-equals', state: 'open', value: 'starting' }
},
{
attr: 'data-ending-style',
severity: 'optional',
condition: { when: 'state-equals', state: 'closed', value: 'ending' }
}
],
aria: [
{
attr: 'aria-label',
value: v.translationRef('#?components.date-picker.calendar|Date calendar'),
severity: 'recommended'
}
],
keyboard: [
{ key: 'Escape', action: 'close' },
{ key: 'Tab', action: 'focus-next' },
{ key: 'Shift+Tab', action: 'focus-prev' }
]
} }
] ]
} as const satisfies Morfo; } as const satisfies Morfo;

@ -23,6 +23,10 @@
onOpenChange = () => {}, onOpenChange = () => {},
onOpenChangeComplete = () => {}, onOpenChangeComplete = () => {},
closeOnDateSelect = true, closeOnDateSelect = true,
mode = 'inline',
clearButton = false,
cancelButton = false,
closeButton = false,
validate, validate,
onInvalid, onInvalid,
minValue, minValue,
@ -133,6 +137,10 @@
readonlySegments: readableActive(() => readonlySegments), readonlySegments: readableActive(() => readonlySegments),
required: readableActive(() => required), required: readableActive(() => required),
closeOnDateSelect: readableActive(() => closeOnDateSelect), closeOnDateSelect: readableActive(() => closeOnDateSelect),
mode: readableActive(() => mode),
clearButton: readableActive(() => clearButton),
cancelButton: readableActive(() => cancelButton),
closeButton: readableActive(() => closeButton),
granularity: readableActive(() => granularity), granularity: readableActive(() => granularity),
hideTimeZone: readableActive(() => hideTimeZone), hideTimeZone: readableActive(() => hideTimeZone),
hourCycle: readableActive(() => hourCycle), hourCycle: readableActive(() => hourCycle),
@ -158,11 +166,13 @@
}); });
// ── Popover provider wired to the shared open state ──────────────────────── // ── Popover provider wired to the shared open state ────────────────────────
// In modal mode the popover ignores outside-click and Escape dismissal —
// the user must commit via Save/Close or revert via Cancel from the footer.
PopoverProvider.create({ PopoverProvider.create({
id: readableActive(() => `${id}-popover`), id: readableActive(() => `${id}-popover`),
open: sharedOpen, open: sharedOpen,
onOpenChangeComplete: readableActive(() => onOpenChangeComplete), onOpenChangeComplete: readableActive(() => onOpenChangeComplete),
modal: readableActive(() => false) modal: readableActive(() => mode === 'modal')
}); });
// ── DateField provider wired to the shared value / placeholder / config ─── // ── DateField provider wired to the shared value / placeholder / config ───

@ -20,7 +20,7 @@ import { Soma } from '$soma/core/soma.svelte';
import { createSomaRuntime, type SomaRuntimeSources } from '$soma/runtime.svelte'; import { createSomaRuntime, type SomaRuntimeSources } from '$soma/runtime.svelte';
import type { Direction, OnChangeFn } from '../../types'; import type { Direction, OnChangeFn } from '../../types';
import { DatePickerProvider } from './date-picker-provider.svelte'; import { DatePickerProvider, type DatePickerMode } from './date-picker-provider.svelte';
function withEffectRoot<T>(fn: () => T): { result: T; cleanup: () => void } { function withEffectRoot<T>(fn: () => T): { result: T; cleanup: () => void } {
let result!: T; let result!: T;
@ -72,6 +72,10 @@ function createDatePickerOpts() {
readonlySegments: state<EditableSegmentPart[]>([]), readonlySegments: state<EditableSegmentPart[]>([]),
required: state(false), required: state(false),
closeOnDateSelect: state(true), closeOnDateSelect: state(true),
mode: state<DatePickerMode>('inline'),
clearButton: state(false),
cancelButton: state(false),
closeButton: state(false),
granularity: state<Granularity | undefined>(undefined), granularity: state<Granularity | undefined>(undefined),
hideTimeZone: state(false), hideTimeZone: state(false),
hourCycle: state<HourCycle | undefined>(undefined), hourCycle: state<HourCycle | undefined>(undefined),

@ -1,3 +1,5 @@
import { untrack } from 'svelte';
import { watch } from 'runed';
import { context, type ProviderOpts } from '../../provider'; import { context, type ProviderOpts } from '../../provider';
import { createAttrs } from '$uix/morfo'; import { createAttrs } from '$uix/morfo';
import { boolToEmptyStrOrUndef, boolToStr, getDataOpenClosed } from '$adom'; import { boolToEmptyStrOrUndef, boolToStr, getDataOpenClosed } from '$adom';
@ -16,6 +18,14 @@ import type {
WeekStartsOn WeekStartsOn
} from '$libs/days'; } from '$libs/days';
/**
* Picker interaction mode. `inline` (default) auto-closes the popover when
* a date is selected (subject to `closeOnDateSelect`). `modal` keeps the
* popover open until the user explicitly confirms via the Save action in
* the footer; Cancel restores the value snapshot taken at open.
*/
export type DatePickerMode = 'inline' | 'modal';
// ── Contract ───────────────────────────────────────────────────────────────── // ── Contract ─────────────────────────────────────────────────────────────────
import { datePickerMorfo } from '../../../morfo/components/date-picker'; import { datePickerMorfo } from '../../../morfo/components/date-picker';
@ -47,6 +57,10 @@ interface DatePickerOpts
readonlySegments: EditableSegmentPart[]; readonlySegments: EditableSegmentPart[];
required: boolean; required: boolean;
closeOnDateSelect: boolean; closeOnDateSelect: boolean;
mode: DatePickerMode;
clearButton: boolean;
cancelButton: boolean;
closeButton: boolean;
// Format / i18n // Format / i18n
granularity: Granularity | undefined; granularity: Granularity | undefined;
hideTimeZone: boolean; hideTimeZone: boolean;
@ -99,6 +113,12 @@ export class DatePickerProvider {
return new DatePickerProvider(opts); return new DatePickerProvider(opts);
} }
/**
* Snapshot of `value` captured on the OPEN edge. Used by `cancel()` to
* revert any mid-session edits back to the value the popover opened with.
*/
private valueOnOpen: DateValue | undefined = undefined;
private constructor(opts: DatePickerOpts) { private constructor(opts: DatePickerOpts) {
this.opts = opts; this.opts = opts;
this.soma = Soma.require(); this.soma = Soma.require();
@ -117,6 +137,16 @@ export class DatePickerProvider {
owner: this, owner: this,
context: DatePickerProvider.ctx context: DatePickerProvider.ctx
}); });
// Capture value on the opening edge so `cancel()` can revert.
watch(
() => this.opts.open.current,
(now, prev) => {
if (now && !prev) {
this.valueOnOpen = untrack(() => this.opts.value.current);
}
}
);
} }
readonly validationStatus = $derived.by(() => { readonly validationStatus = $derived.by(() => {
@ -137,13 +167,45 @@ export class DatePickerProvider {
readonly isInvalid = $derived.by(() => this.validationStatus !== false); readonly isInvalid = $derived.by(() => this.validationStatus !== false);
/** Close the popover if `closeOnDateSelect` is `true`. Called by the Calendar. */ /**
* Close the popover if `closeOnDateSelect` is `true` AND mode is inline.
* Modal mode never auto-closes on selection — the user must commit via
* the explicit Save action in the footer.
*/
handleDateSelect(): void { handleDateSelect(): void {
if (this.opts.mode.current === 'modal') return;
if (this.opts.closeOnDateSelect.current) { if (this.opts.closeOnDateSelect.current) {
this.opts.open.current = false; this.opts.open.current = false;
} }
} }
// ── Action helpers (footer wiring) ────────────────────────────────────────
/**
* Commit the current value and close the popover. The default behavior
* for a "Save" or "Done" button in the footer, especially in modal mode.
*/
commit(): void {
this.opts.open.current = false;
}
/**
* Discard any edits made since the popover opened and close. Used by
* the Cancel button in the footer.
*/
cancel(): void {
this.opts.value.current = this.valueOnOpen;
this.opts.open.current = false;
}
/**
* Reset the value to undefined without closing the popover. Used by
* the Clear button in the footer.
*/
clear(): void {
this.opts.value.current = undefined;
}
readonly props = $derived.by(() => readonly props = $derived.by(() =>
this.runtimePart.assert({ this.runtimePart.assert({
...this.runtimePart.props, ...this.runtimePart.props,

@ -3,6 +3,10 @@ export { default as Provider } from './components/date-picker.svelte';
export { default as Trigger } from './components/date-picker-trigger.svelte'; export { default as Trigger } from './components/date-picker-trigger.svelte';
export { default as Calendar } from './components/date-picker-calendar.svelte'; export { default as Calendar } from './components/date-picker-calendar.svelte';
// Provider class (action helpers: commit / cancel / clear / handleDateSelect)
export { DatePickerProvider } from './date-picker-provider.svelte';
export type { DatePickerMode } from './date-picker-provider.svelte';
// Popover surface (shared via composed PopoverProvider) // Popover surface (shared via composed PopoverProvider)
export { export {
Content, Content,

@ -47,10 +47,39 @@ type DatePickerBaseProps = {
onOpenChangeComplete?: OnChangeFn<boolean>; onOpenChangeComplete?: OnChangeFn<boolean>;
/** /**
* Whether the popover closes automatically when a date is selected in the * Whether the popover closes automatically when a date is selected in the
* Calendar. * Calendar. Has no effect in `mode='modal'` — modal pickers never
* auto-close.
* @default true * @default true
*/ */
closeOnDateSelect?: boolean; closeOnDateSelect?: boolean;
/**
* Interaction mode.
* - `'inline'` (default): selection auto-applies; `closeOnDateSelect`
* controls whether the popover dismisses on selection. Footer actions
* are optional ergonomic affordances.
* - `'modal'`: selection edits a buffer; the popover stays open until
* the user confirms via the Save action in the footer. Cancel reverts
* to the value the popover opened with.
* @default 'inline'
*/
mode?: import('./date-picker-provider.svelte').DatePickerMode;
/**
* Whether the footer renders a Clear button (resets value, keeps popover
* open). @default false
*/
clearButton?: boolean;
/**
* Whether the footer renders a Cancel button (reverts to value-at-open
* and closes). @default false
*/
cancelButton?: boolean;
/**
* Whether the footer renders a Close/Save button (commits current value
* and closes). In `mode='modal'` this is forced ON so the user always has
* a way out — modal pickers don't dismiss on outside click or Escape.
* @default false (inline) · forced true (modal)
*/
closeButton?: boolean;
// ── Validation ── // ── Validation ──
/** Custom validator — return an error message when invalid. */ /** Custom validator — return an error message when invalid. */

@ -15,6 +15,7 @@
import { popoverMorfo } from '@/uix/morfo/components/popover'; import { popoverMorfo } from '@/uix/morfo/components/popover';
import { getActiveUix } from '$active-uix'; import { getActiveUix } from '$active-uix';
import { CalendarDate, type DateMatcher, type DateValue, type WeekStartsOn } from '$libs/days'; import { CalendarDate, type DateMatcher, type DateValue, type WeekStartsOn } from '$libs/days';
import PickerActions from './picker-actions.svelte';
const uix = getActiveUix(); const uix = getActiveUix();
void uix; void uix;
@ -49,6 +50,10 @@
let disabled = $state(false); let disabled = $state(false);
let readonly = $state(false); let readonly = $state(false);
let closeOnDateSelect = $state(true); let closeOnDateSelect = $state(true);
let mode = $state<'inline' | 'modal'>('inline');
let clearButton = $state(false);
let cancelButton = $state(false);
let closeButton = $state(false);
let disableDaysOutsideMonth = $state(true); let disableDaysOutsideMonth = $state(true);
let fixedWeeks = $state(false); let fixedWeeks = $state(false);
let pagedNavigation = $state(false); let pagedNavigation = $state(false);
@ -420,6 +425,10 @@
{size} {size}
{variant} {variant}
{color} {color}
{mode}
{clearButton}
{cancelButton}
{closeButton}
minValue={minValue} minValue={minValue}
maxValue={maxValue} maxValue={maxValue}
weekStartsOn={resolvedWeekStartsOn} weekStartsOn={resolvedWeekStartsOn}
@ -585,6 +594,25 @@
<span data-uix-switch><input type="checkbox" bind:checked={disabled} /><span data-uix-switch-label>disabled</span></span> <span data-uix-switch><input type="checkbox" bind:checked={disabled} /><span data-uix-switch-label>disabled</span></span>
<span data-uix-switch><input type="checkbox" bind:checked={readonly} /><span data-uix-switch-label>readonly</span></span> <span data-uix-switch><input type="checkbox" bind:checked={readonly} /><span data-uix-switch-label>readonly</span></span>
</label> </label>
<label data-uix-control>
<span data-uix-control-label>mode</span>
<span data-uix-chips role="radiogroup">
<label data-uix-chip data-active={mode === 'inline'}>
<input type="radio" bind:group={mode} value="inline" />
inline
</label>
<label data-uix-chip data-active={mode === 'modal'}>
<input type="radio" bind:group={mode} value="modal" />
modal
</label>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>footer buttons</span>
<span data-uix-switch><input type="checkbox" bind:checked={clearButton} /><span data-uix-switch-label>clear</span></span>
<span data-uix-switch><input type="checkbox" bind:checked={cancelButton} /><span data-uix-switch-label>cancel</span></span>
<span data-uix-switch><input type="checkbox" bind:checked={closeButton} /><span data-uix-switch-label>close</span></span>
</label>
<label data-uix-control> <label data-uix-control>
<span data-uix-control-label>calendar behavior</span> <span data-uix-control-label>calendar behavior</span>
<span data-uix-switch><input type="checkbox" bind:checked={closeOnDateSelect} /><span data-uix-switch-label>close on select</span></span> <span data-uix-switch><input type="checkbox" bind:checked={closeOnDateSelect} /><span data-uix-switch-label>close on select</span></span>
@ -994,6 +1022,7 @@
</div> </div>
{/each} {/each}
</div> </div>
<PickerActions />
{/snippet} {/snippet}
<style> <style>

@ -0,0 +1,111 @@
<script lang="ts">
import { DatePickerProvider } from '$soma/components/date-picker';
// Footer for the DatePicker calendar. Lives inside the provider tree
// (rendered from the Calendar snippet) so it can pull options + helpers
// straight from context.
//
// Buttons are configured via props on the picker:
// - `clearButton` — Clear (reset value, keep open)
// - `cancelButton` — Cancel (revert to value-at-open, close)
// - `closeButton` — Close / Save (commit current value, close)
//
// In `mode='modal'` Close is forced on so the user always has a way out:
// modal popovers ignore outside-click and Escape.
//
// If no buttons are enabled (and the picker is not modal), this footer
// renders nothing.
const provider = DatePickerProvider.require();
const mode = $derived(provider.opts.mode.current);
const showClear = $derived(provider.opts.clearButton.current);
const showCancel = $derived(provider.opts.cancelButton.current);
const showClose = $derived(provider.opts.closeButton.current || mode === 'modal');
const visible = $derived(showClear || showCancel || showClose);
</script>
{#if visible}
<div class="picker-footer" data-date-picker-footer data-mode={mode}>
{#if showClear}
<button
type="button"
class="picker-footer-button"
data-tone="ghost"
onclick={() => provider.clear()}
>
Clear
</button>
{/if}
<div class="picker-footer-spacer"></div>
{#if showCancel}
<button
type="button"
class="picker-footer-button"
data-tone="ghost"
onclick={() => provider.cancel()}
>
Cancel
</button>
{/if}
{#if showClose}
<button
type="button"
class="picker-footer-button"
data-tone="solid"
onclick={() => provider.commit()}
>
Close
</button>
{/if}
</div>
{/if}
<style>
.picker-footer {
display: flex;
align-items: center;
gap: var(--uix-space-2, 0.5rem);
padding-block-start: var(--uix-space-3, 0.75rem);
margin-block-start: var(--uix-space-3, 0.75rem);
border-block-start: 1px solid var(--color-border-subtle);
}
.picker-footer-spacer {
flex: 1 1 auto;
}
.picker-footer-button {
inline-size: max-content;
min-block-size: var(--control-height-sm);
padding-inline: var(--uix-space-3, 0.75rem);
border-radius: var(--radius-md);
border: var(--border-width) solid transparent;
background: transparent;
color: var(--color-content-primary);
font: inherit;
font-size: var(--uix-text-sm);
cursor: pointer;
transition:
background var(--duration-fast) var(--ease-default),
border-color var(--duration-fast) var(--ease-default),
color var(--duration-fast) var(--ease-default);
}
.picker-footer-button[data-tone='ghost']:hover {
background: var(--color-surface-overlay);
}
.picker-footer-button[data-tone='solid'] {
border-color: var(--color-primary-border);
background: var(--color-primary-solid);
color: var(--color-content-on-solid);
}
.picker-footer-button[data-tone='solid']:hover {
background: var(--color-primary-solid-hover);
}
.picker-footer-button:focus-visible {
outline: var(--focus-ring-width) solid var(--color-primary-border);
outline-offset: var(--focus-ring-offset);
}
</style>
Loading…
Cancel
Save

Powered by TurnKey Linux.