Adds external comparison table (WAI-ARIA / React Aria / Ark UI / Base UI / Bits) with sources and a Gaps table with disposition markers, bringing the css-field audit scorecard to parity with number-field (only the systemic E-2.2 css-import rule + R-1.3 readonly warning remain, both shared with the reference component). Smoke PASS for /uix/components/css-field. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>active-uix
parent
7ff1738a12
commit
9452bc2545
@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Clamped virtual anchor for the properties Popover.
|
||||
*
|
||||
* The panel is anchored to the ACTIVE block but must always sit on a chosen
|
||||
* `side` (the main panel to the right, the appearance popover to the left). A
|
||||
* full-width block fills the content column, so in a narrow window the gutter on
|
||||
* that side is smaller than the panel — a plain block anchor then makes the
|
||||
* Popover FLIP to the opposite side (the panel jumps over the doc sidebar). The
|
||||
* user wants it to stay put ("que apareciera a la derecha").
|
||||
*
|
||||
* Fix: hand the Popover a VIRTUAL anchor (a floating-ui `Measurable`) whose
|
||||
* rect is CLAMPED on the main axis so the panel always fits the viewport on
|
||||
* `side`. When the gutter is too small the anchor slides inward, so the panel
|
||||
* overlaps the content's edge instead of flipping — `flip()` never triggers
|
||||
* because the placement always fits. A zero-width strip spanning the block's
|
||||
* vertical extent keeps `align='center'` centred on the block's mid-height.
|
||||
*
|
||||
* floating-ui calls `getBoundingClientRect` on every autoUpdate cycle, so scroll
|
||||
* and resize re-clamp automatically (`contextElement` = the block, the element
|
||||
* autoUpdate observes for scroll parents). The rect depends only on the block +
|
||||
* viewport — never on the panel — so moving it can't feed back into a resize loop.
|
||||
*
|
||||
* NOTE on the type: the soma floating layer accepts `Measurable` as its anchor
|
||||
* (`FloatingContent.customAnchor: string | HTMLElement | null | Measurable`),
|
||||
* but the public `<Popover.Content customAnchor>` prop narrows that to
|
||||
* `string | HTMLElement | null`. The capability is real at runtime; consumers
|
||||
* cast the result to `HTMLElement` at the call site (we don't modify soma).
|
||||
*/
|
||||
|
||||
/** Minimal floating-ui virtual-element shape (avoids a runtime floating-ui dep). */
|
||||
export interface Measurable {
|
||||
getBoundingClientRect(): DOMRect;
|
||||
contextElement?: Element;
|
||||
}
|
||||
|
||||
export interface ClampedAnchorOptions {
|
||||
/** Which side the panel opens toward — the axis edge that gets clamped. */
|
||||
side: 'left' | 'right';
|
||||
/** The panel's width (must match `<Popover.Content width>`). */
|
||||
panelWidth: number;
|
||||
/** Gap between block and panel (must match `sideOffset`). */
|
||||
gap: number;
|
||||
/** Viewport edge padding to keep (match `collisionPadding`). */
|
||||
padding: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a clamped virtual anchor over `block`. `win` resolves the viewport width
|
||||
* (pass the block's own window so it stays iframe / popup correct).
|
||||
*/
|
||||
export function clampedBlockAnchor(
|
||||
block: HTMLElement,
|
||||
win: Window,
|
||||
opts: ClampedAnchorOptions
|
||||
): Measurable {
|
||||
return {
|
||||
contextElement: block,
|
||||
getBoundingClientRect() {
|
||||
const b = block.getBoundingClientRect();
|
||||
const vw = win.innerWidth;
|
||||
let x: number;
|
||||
if (opts.side === 'right') {
|
||||
// panel.left = x + gap → keep panel.left + panelWidth <= vw - padding.
|
||||
const maxRight = vw - opts.panelWidth - opts.gap - opts.padding;
|
||||
x = Math.min(b.right, maxRight);
|
||||
} else {
|
||||
// panel.right = x - gap → keep panel.right - panelWidth >= padding.
|
||||
const minLeft = opts.padding + opts.panelWidth + opts.gap;
|
||||
x = Math.max(b.left, minLeft);
|
||||
}
|
||||
// Zero-width strip at the clamped x, spanning the block's height.
|
||||
return {
|
||||
x,
|
||||
y: b.top,
|
||||
width: 0,
|
||||
height: b.height,
|
||||
top: b.top,
|
||||
right: x,
|
||||
bottom: b.bottom,
|
||||
left: x,
|
||||
toJSON() {
|
||||
return this;
|
||||
}
|
||||
} as DOMRect;
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Reusable segmented control for the palabras panel — a single-select
|
||||
* `ToggleGroup` with the demo's appearance (`outline` + `attached`, `xs`).
|
||||
* Each option renders as an ICON (icon-only, the label becomes the accessible
|
||||
* name + tooltip) when it carries an `icon`, otherwise as its text label.
|
||||
*
|
||||
* Every `pills` field routes through here (alignment with icons, fit with
|
||||
* text, …). Palabras-scoped on purpose: it bundles panel-specific defaults,
|
||||
* it is NOT a new framework primitive — the framework `ToggleGroup` stays
|
||||
* untouched and we just compose it.
|
||||
*/
|
||||
import { ToggleGroup } from '$uix/eidos/components/toggle-group';
|
||||
import type { PalabrasFieldOption } from './types';
|
||||
|
||||
let {
|
||||
value,
|
||||
options,
|
||||
disabled = false,
|
||||
ariaLabel,
|
||||
onValueChange
|
||||
}: {
|
||||
/** Currently selected option value (stringified). */
|
||||
value: string | undefined;
|
||||
readonly options: readonly PalabrasFieldOption[];
|
||||
disabled?: boolean;
|
||||
ariaLabel?: string;
|
||||
/** Fired with the picked option's stringified value, or `undefined` when the
|
||||
* active item is re-clicked (deselected) — lets the field clear / restore. */
|
||||
onValueChange: (value: string | undefined) => void;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="xs"
|
||||
attached
|
||||
{disabled}
|
||||
aria-label={ariaLabel}
|
||||
value={value == null ? [] : [value]}
|
||||
onValueChange={(v) => onValueChange(v[0])}
|
||||
>
|
||||
{#each options as opt (opt.value)}
|
||||
{@const Icon = opt.icon}
|
||||
<ToggleGroup.Item
|
||||
value={String(opt.value)}
|
||||
iconOnly={!!Icon}
|
||||
aria-label={opt.label}
|
||||
title={opt.label}
|
||||
>
|
||||
{#if Icon}<Icon />{:else}{opt.label}{/if}
|
||||
</ToggleGroup.Item>
|
||||
{/each}
|
||||
</ToggleGroup>
|
||||
Loading…
Reference in new issue