feat(Overflow): add new component#220
Merged
Merged
Conversation
|
commit: |
… Root - isVisible now ranks within non-disabled siblings instead of using raw registry index, fixing items past a disabled sibling being incorrectly hidden when they still fit within capacity - bind slotProps.attrs to Root's Atom so non-renderless consumers see data-overflow / data-priority hooks on the DOM - forward gap as a getter to createOverflow so prop changes recompute capacity instead of capturing the initial snapshot - enable reactive registry so ticket.index tracks reindexes; OverflowItem cleans the previous index before writing a new one to prevent orphan widths from accumulating after a sibling unmounts - re-measure the indicator with useResizeObserver when its content or styles change so the reserved width stays accurate as the count grows - align data-attr typing with Tabs precedent (true | undefined), add @module/@remarks on OverflowRoot, @see on barrel exports, default renderless to false, and remove docs FAQ guidance to use gap for reservation
Replaces the hand-rolled `:style="{ display: isHidden ? 'none' : null }"`
with `v-show="isVisible"`. Functionally equivalent — Vue's v-show does
the same display-none-with-original-restore — but matches the precedent
used by Breadcrumbs and Avatar for components that must stay mounted to
preserve registered ticket state.
Codifies the convention in PHILOSOPHY §10.11 and components.md: v-show is
correct (not the exception) for registry-driven visibility, load-state
preservation, and virtualization. Bans the hand-rolled :style form.
Drops the `_priority` / `_disabled` named consts in favor of inline `toRef(() => prop)` at the provide call site, matching TabsRoot, BreadcrumbsRoot, ImageRoot, and the rest of the codebase. The temps were used exactly once each — no readability or reuse benefit.
Anatomy is for visualizing the available components, not a runnable example — drop the items array, v-for, slot vars, and props so the fence reads as a component-tree map matching ExpansionPanel's pattern.
…dent - Rename `measureSelf` → `measure` to match the single-verb convention used by every component-internal function (close, open, toggle, …). - Route Atom element access through `toElement(atomRef.value?.element)` per the Image / Carousel × 4 precedent instead of a raw HTMLElement cast — joins the `toElement-template-refs.md` cleanup family. - Rename the resulting ref `elementRef` → `el` to match the same precedents (single-word `el` everywhere else in the codebase). - Inline the now-redundant element extraction in `measure` to read `el.value` directly.
Adds a "Consuming AtomExpose in the same SFC" subsection covering the
canonical form for component authors who wrap an Atom and need its
element for measurement, observers, focus, or popover anchoring:
- Always route through `toElement` (#v0/composables/toElement)
- Wrap in `toRef(() => …)` so downstream consumers stay reactive
- Name the ref `el` (or `{position}El` when there are multiple atoms)
Cites worked precedents (Image, Carousel × 4, OverflowIndicator) and
points at the `toElement-template-refs.md` saved memory for the legacy
raw-cast holdouts (Tabs, Treeview, …).
Also fixes an inaccurate inline comment in the existing AtomExpose
section: `rootAtom.value?.element` resolves to the HTMLElement directly
because Vue auto-unwraps refs surfaced via `defineExpose` — no second
`.value` required.
Tickets no longer carry a per-instance `isVisible` Ref. The Root's `registry.register` override (which wrapped the base register to attach `ticket.isVisible = toRef(() => isVisible(ticket.index))`) was the only place in v0 that mutated registry internals — zero precedent in source. Replaces the post-registration mutation with consumer-side computation: - OverflowItem reads `root.isVisible(ticket.index)` directly. - OverflowIndicator filters `(_, index) => !root.isVisible(index)`. - OverflowTicket loses its `isVisible` field; the `disabled` field stays. Identical observable behavior, simpler ticket type, no override of the createRegistry public surface. Test renamed to assert the Root context exposes `isVisible(index)` instead of the dropped per-ticket field. Also drops a redundant `// reactive: true wraps each ticket…` comment from OverflowRoot — Splitter and Carousel pass the same option without commentary, and the rule is documented in composables.md.
Replaces raw `!== null` comparisons on `lastMeasuredIndex` with
`!isNull(...)` per `#v0/utilities`, matching style.md and PHILOSOPHY
§2.3 ("Utility guards over raw comparisons"). Two sites: the watch
deduper and the onBeforeUnmount cleanup.
The earlier "Consuming AtomExpose" subsection covered the toRef-derived
element ref but left the `useTemplateRef<AtomExpose>` holder undefined.
Adds a leading bullet codifying the holder name (`atomRef` for a single
Atom, `{role}Ref` / `{role}Atom` / role-only for multiple) and lists
the precedents — plus the Carousel files (CarouselNext, CarouselItem,
CarouselLiveRegion) as anti-precedent because they call the holder
`rootEl` even though it carries an AtomExpose, not an HTMLElement.
Two undocumented conventions caught in review: - Anatomy fences are component-tree shells, not runnable examples — no data arrays, v-for, props beyond `:as`, slot vars, or runtime values. The runnable preview lives in `## Examples`; Anatomy is structural. Cites ExpansionPanel as reference. - Reactive context fields derived from destructured props are written inline at the provide call (`disabled: toRef(() => disabled)`), not pre-bound to a `_prop` const. The named local is only justified when the Ref is reused. Cites Tabs/Breadcrumbs/Image/Overflow precedent.
…arding
§4.3 already specifies the receiver side ("type as MaybeRefOrGetter,
read via toValue()") but said nothing about how a destructured prop
gets forwarded into one. With Vue 3.5+ reactive props destructure,
`prop` is reactive when read inside a getter but evaluated once when
read inline at object-literal construction. A real bug surfaced from
exactly this miss in OverflowRoot. Adds a "Caller side" subsection
naming the rule and citing OverflowRoot as the worked example.
Also refreshes §2.3's anti-example. The two cited raw comparisons
(createOverflow:195 and color.ts:17,25,30) have both been migrated;
the only remaining holdouts are inside JSDoc @example blocks in
useNotifications:503,537. Updates the citation accordingly.
OverflowRoot routed `containerRef.value?.element` through a raw `as Element | undefined` cast and OverflowItem read `atomRef.value?.element` twice without an `el` ref. Both miss the just-codified components.md "Consuming AtomExpose" rule. - OverflowRoot.vue:80 — `container: () => toElement(containerRef.value?.element)`. - OverflowItem.vue — `el = toRef(() => toElement(atomRef.value?.element) ?? null)`, consumed in the watch dep array and the measure call. Folds the if/else around `measure` into a ternary now that the casts are gone. - OverflowRoot.vue:82 — `reserved` passes the `indicatorWidth` Ref directly. The `() => indicatorWidth.value` getter wrapper did nothing the Ref didn't already do via toValue and broke the visual symmetry by being the only param that wrapped a Ref's .value.
`since` ossifies a fictional ship version on pre-release entries — a later alpha/beta cut can land before the feature actually ships and nobody re-checks the field. The version a feature shipped in is only known the moment it goes stable. - new-feature-checklist.md: only `level` and `category` (plus optional `notes`) on draft/preview. `since` is added in the same PR that promotes the entry to stable. Explanatory paragraph + revised example reflect this. - maturity.json: strip `since: "1.0.0-alpha.1"` from Overflow's preview entry — the version was a placeholder for an unshipped release.
The basic, popover, and priority-end blocks were carrying `.vue` extensions and `1` order numbers — that's the multi-file form. The single-file form drops both: the renderer auto-appends `.vue` and order is meaningless when there's only one file. Matches Breadcrumbs, ExpansionPanel, Checkbox precedent.
Two rules contradicted what every doc page on the site actually does: - meta-indent rule said ` - name:` (2-space). Every doc page on the site (overflow, pagination, breadcrumbs, …) uses `- name:` at column 0. Both forms are valid YAML and parse identically; column 0 is the de-facto standard. Aligns the rule to practice rather than reformatting every page. - Usage section had two contradictory specs: the structure list said "code fence (not a live example)", the section-content table said "::: example with basic.vue". Pages diverge — Breadcrumbs/ ExpansionPanel/Checkbox use ::: example; Pagination/Overflow use a code fence with prose. Both are legitimate (the latter when the section has explanatory text). Rule now permits either form with guidance on when to pick which.
…tting Updates the maturity rule from "omit since on draft/preview" to "set since: null". Two reasons to keep the key with a null value over dropping it: - Schema uniformity. Every entry has the same shape regardless of level, so tooling can iterate without branching on key presence. - Visibility. `"since": null` reads as a deliberate "fill in on release" placeholder. An omitted key is invisible and easy to forget during the stable-promotion PR. Updates the example, the rationale subsection, and the level-mapping list. Restores `since: null` on the Overflow entry that was stripped in the previous pass.
Earlier guidance tied `since` to the stable-promotion PR. That's wrong: `since` records the first release version the feature appeared in, regardless of level. A `preview` feature gets a real `since` the moment a release including it is cut — `level` stays `preview` until the API stabilises, but `since` is permanent. Updated example shows two preview entries side-by-side: one already shipped (real version), one not yet released (still null). Updated rationale and level-mapping list reflect: - author writes `since: null` regardless of level - maintainer cutting the first release flips null → the release version - promotion `preview` → `stable` does not touch `since`
Two related restrictions added to docs.md: - No backticks in markdown headers. Headers feed the TOC and the URL anchor; inline-code styling renders inconsistently in both and the anchor slug picks up the surrounding chars. - No backticks wrapping link text. The link styling already signals the identifier; the backticks layer redundant — and in many themes broken — formatting. Inline code in regular prose stays fine. Lists existing violations (select.md, composables index.md, why-vuetify0.md, create-trinity.md) as a sweep candidate. Fixes the one violation in the Overflow page that motivated the rule.
The previous chain `toElement(atomRef.value?.element) as HTMLElement | null ?? null` papered over two distinct conversions with one cast. toElement returns Element | undefined; the cast asserted HTMLElement | null. TS thought the value was already nullable so the `?? null` looked redundant, but runtime-wise it was the load-bearing piece — it converted undefined to null. The cast was a lie that obscured what the chain was doing. Drops the cast. `el` is now `Ref<Element | null>` honestly. The single HTMLElement-only access (`el.value.offsetWidth` in measure) gets a boundary cast at the use site, which is honest about why HTMLElement is needed there. Image/ImageRoot.vue and Carousel × 4 still carry the legacy chain — sweep candidate.
The codified "Consuming AtomExpose" rule recommended the same chain that was just fixed in OverflowIndicator. Updates the canonical example to drop the cast and adds a bullet explaining why: toElement returns Element | undefined, so the historical `as HTMLElement | null ?? null` chain is misleading — TS thinks the ?? null is no-op but it's actually doing the undefined→null conversion the cast lied about. Renames "Worked precedents" to split current form (Overflow) from legacy form (Image, Carousel × 4) for the inevitable sweep.
basic.vue, priority-end.vue, and avatar-group.vue each rendered an
Input.Root range slider that drove a `:style="{ width: ... }"` on the
Overflow.Root. The DocsExample frame already exposes a resize handle,
so the in-example slider was a redundant second control mechanism.
Removes:
- The slider markup, label, and width state from each example
- The wrapping `<div class="space-y-4">` (single child now)
- The `Input` import and `shallowRef` import where they became unused
- `transition-all` on the Overflow.Root (was there to smooth the slider
driven width changes; the resize handle doesn't need it)
Updates the basic.vue role caption in overflow.md to drop the now-stale
"width slider" mention.
…ders - users.ts grows from 8 to 40 entries (CS pioneers, hues spread evenly every 9° around the wheel) so the avatar-group demo actually shows a `+N` indicator at typical container widths. - popover.vue popover items get `whitespace-nowrap` — the `block` class let long tag names wrap mid-word inside the popover; nowrap keeps each value on a single line. - Examples headers shortened to scannable labels: "Basic tag row" → "Tags", "Avatar group with overflow indicator" → "Avatar group", "Indicator opens a popover of hidden items" → "Popover of hidden items", `priority="end" for trailing-priority lists` → "End priority". The prose under each header still carries the long-form explanation.
The Overflow.Root carried `:gap="-8"` while each Item had `marginInlineStart: -8px`. The margin was already captured in each item's measured width via getComputedStyle().marginLeft (so per-item width = 32 + -8 = 24px). The gap prop subtracted the overlap a second time, telling capacity the 40 avatars take ~648px when they actually take ~960px — wider than the example container, so overflow never triggered. createOverflow's `gap` mirrors the container's CSS gap, not visual overlap. With no `gap-*` utility on the container, the prop should be omitted (defaults to 0). The marginInlineStart still produces the visual overlap and is correctly counted once in the per-item measure. Updates the example prose accordingly.
…ty-end to recipes
Page restructure per feedback: Examples section trimmed from four
demos to the two strongest, both reworked into copy-paste-ready
patterns. priority-end becomes a Recipe.
Examples (kept and enhanced):
- avatar-group: now renders as a semantic <ul>/<li> with `aria-label`
on the list, `sr-only` full names alongside the visible initials,
and a labelled `+N` indicator. Comments left out of the file but
the prose now explains the no-gap rule (mirror only CSS gap, never
the per-item visual overlap) so readers don't repeat the bug we
hit during review.
- popover: same semantic <ul>/<li> upgrade; activator gets
type="button" + `aria-label="Show {N} more topics"`; popover
content gets `max-h-64 overflow-y-auto` for the many-hidden edge
case. Tag list trimmed to 16 realistic department names.
Recipes (added):
- "Trailing-priority lists with priority='end'" now lives here as a
collapsed `::: example` referencing priority-end.vue. The full
prose about the announce-order tradeoff and breadcrumb redirect
comes along.
Removed:
- basic.vue example. The Usage section's inline code fence already
shows the minimal usage; basic was a less-interesting subset of
the popover demo and the page reads tighter without it.
Usage section prose expanded from one paragraph (mechanics only) to
two — the first explains why CSS `text-overflow: ellipsis`,
`overflow: scroll`, and container queries can't replace this; the
second covers the three-component skeleton and the ResizeObserver-
driven mount/unmount of the indicator.
priority-end.vue itself updated to a relative-timestamp message list
(2 weeks ago → 1 hour ago) — clearer narrative of what `priority="end"`
does than the prior generic message strings.
Both examples (avatar-group and popover) had been switched to `<Overflow.Root as="ul">` + `<Overflow.Item as="li">` for semantic list announcements. In the live page, every item rendered with `display: none` and stayed hidden — `total: 0`, `capacity: 0`, `registry.size: 40` per Vue devtools introspection. The widths Map in createOverflow never populated. The `as="span"` form had worked. Reverting to `<div>`/`<span>` and using `role="list"` + `role="listitem"` gives the same a11y semantics without triggering the measurement chicken-egg. Both examples now render correctly: 27/41 contributors visible (`+14` indicator), 9/17 topics visible (`+8 more` popover trigger). Updates the Accessibility table on the docs page to recommend the role-based form too. The `as="ul"`/`as="li"` advice was unsafe. The underlying createOverflow/OverflowItem bug is logged separately in `~/.claude/projects/-home-john-sites-0/memory/overflow-li-rendering-bug.md` for follow-up.
Backs out the role-list / consolidation pair from earlier in this session: - Re-adds basic.vue and re-promotes it to the leading example. priority-end moves back from Recipes to Examples. Examples are now basic / avatar-group / popover / priority-end again. - Drops the role="list" + role="listitem" + aria-label additions from avatar-group.vue and popover.vue, plus the contributor-count header and sr-only name span on avatar-group. Accessibility table reverts to "pass as='ul'/as='li' for list semantics". - popover.vue tag list swaps the data/devops/security/qa entries for salesforce/zendesk/jira/confluence. - Trims the verbose Usage prose back to the two-sentence form and drops the inline overflow/scroll/container-query justification — the intro covers it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Overflowcomponent (Root+Item+Indicator) underpackages/0/src/components/Overflow/, wrapping the existingcreateOverflowcomposable for general-purpose responsive truncation (tag rows, avatar groups, toolbar action lists).Overflow.Rootowns the registry + capacity computation, supportspriority="start" | "end"and a Root-leveldisabledshort-circuit.Overflow.Itemself-registers, self-measures, and togglesdata-hidden+ structuraldisplay: nonewhen off-capacity.Overflow.Indicatorrenders only when overflowing, exposescount+hiddentickets, and self-measures so its width is reserved.BreadcrumbsRoot./components/semantic/overflowwith four examples: basic tag row, multi-file avatar group, indicator-as-popover-activator, andpriority="end"for trailing-priority lists. Maturity bumped fromdraft→preview.