Skip to content

feat: colour tag chips by tag name - #12892

Merged
gauravsoni119 merged 4 commits into
masterfrom
feat/ocisdev-115-coloured-tags
Sep 10, 2026
Merged

feat: colour tag chips by tag name#12892
gauravsoni119 merged 4 commits into
masterfrom
feat/ocisdev-115-coloured-tags

Conversation

@gauravsoni119

Copy link
Copy Markdown
Contributor

Description

Tag chips were all the same grey, so a file's tags were hard to tell apart at a glance and the same tag looked like nothing in particular across the app.

Each chip is now filled with a colour derived from the tag name: hash(tag) % length picks a slot in the theme's new designTokens.tagColorsList, emitted as --oc-color-tag-N. The colour is derived, never stored, so a tag looks the same for every user on every device with no API, storage or migration change. Different tags may collide on one colour; that is accepted, because a tag's label always renders beside its chip and the colour is reinforcement, never the identifier.

Notes on the design:

  • The label colour is computed per fill rather than pinned to a token. The fills come from the theme, so no single token stays legible against all of them, and --oc-color-text-inverse gets it backwards in the light theme. The calculation lives in useTagColor, not in OcTag, because the theme store is a reactive dependency and getComputedStyle is not — resolving it in the component would leave labels stale after a theme switch. The fill is read straight out of tagColorsList rather than from the emitted CSS var, since getHexFromCssVar reports an unresolvable var as #000000 and would hand a pale fill an illegible white label.
  • A filled chip carries oc-tag-filled, which forces the colours of everything nested inside it down as inherit/currentColor. Those nested elements otherwise repaint themselves — a router-link takes the global anchor colour, a raw OcButton its own swatch, .oc-tag .oc-icon > svg a muted fill — which left navy labels and a washed-out close icon on the deep light-theme fills, and labels that were too light on the pale dark-theme ones.
  • OcTag keeps its default appearance. It is the generic badge (the app-store "most recent" pill, instance badges, the disabled-space indicator, the +N counter), so fillColor and labelColor are both opt-in.
  • initializeThemes now merges arrays with replace semantics. deepmerge concatenates by default, which would turn a tagColorsList present in both defaults and a theme into 60 entries and shift every hue.
  • The tag overflow indicator on the files list also became a real, keyboard reachable button showing the hidden tags in a popover instead of opening the sidebar. The popover is sized to its tags: OcDrop is a fixed 300px wide, which left it nearly empty when it held a single chip capped at 80px, with the tags column's right alignment pushing that chip to the far edge. 300px is kept only as a cap so a long tag still truncates.

Important

The palettes in this PR are placeholders. Both theme.json copies gain 30 fills per theme so the feature can be exercised end to end while design's real values are outstanding. They were generated by binary-searching HSL lightness for a target relative luminance per hue, since equal-lightness hues differ a lot in perceived brightness: deep fills with white labels in the light themes, pale fills with black labels in the dark ones. Worst-case label contrast is 6.31:1 (light) and 8.52:1 (dark), higher in both high-contrast themes. They need swapping for design's palette before merge.

Both theme.json copies are needed: web/packages/web-runtime/themes/… is what pnpm build publishes to web/dist/themes, while an ocis binary serves the go:embed-ed services/web/assets/themes/… copy instead (services/web/pkg/server/http/server.go layers it over the embedded core assets). Every theme in a file carries the same list length, so a tag keeps its colour slot across a theme switch.

Related Issue

  • OCISDEV-115 (internal tracker)

Motivation and Context

Tags carry no visual distinction today, so scanning a file list for a particular tag means reading every label. Colouring the chips makes a tag recognisable at a glance and consistent wherever it appears, with nothing persisted and no schema change — tag colour is a pure function of the tag string.

How Has This Been Tested?

  • test environment: local dev stack (docker compose up with owncloud/ocis:dev + pnpm vite), Chrome, light and dark themes, in both regular and high-contrast variants
  • unit tests: 1980 passing across web-pkg, design-system and web-app-files, including new tests for the hash → index mapping (pinned to known-good literals), useTagColor's fill/label pairing, OcTag's opt-in fill, the files-list chips and the overflow popover
  • contrast: every generated fill/label pair was checked against WCAG relative luminance — worst case 6.31:1 in the light themes and 8.52:1 in the dark ones, both above the 4.5:1 text threshold
  • CSS overrides were verified against the compiled bundle rather than the SCSS source, since the rules they beat ship from other packages: .resource-table-tag-overflow.oc-drop (0,2,0) beats .oc-drop (0,1,0), and the oc-tag-filled rules each win on specificity alone rather than by stylesheet order

Screenshots (if appropriate):

To follow — the palette is a placeholder, so screenshots would show colours that are not the final ones.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Technical debt
  • Tests only (no source changes)

Checklist:

  • Code changes
  • Unit tests added
  • Acceptance tests added
  • Documentation ticket raised:

@gauravsoni119
gauravsoni119 requested a review from a team as a code owner September 7, 2026 12:47
@kw-security

kw-security commented Sep 7, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues
Code Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@gauravsoni119
gauravsoni119 force-pushed the feat/ocisdev-115-coloured-tags branch from 37eb28c to fd0f182 Compare September 7, 2026 12:49
@gauravsoni119
gauravsoni119 requested review from a team and LukasHirt and removed request for a team September 7, 2026 12:50
@gauravsoni119 gauravsoni119 self-assigned this Sep 8, 2026

@LukasHirt LukasHirt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH, I am not sure whether I like the increased boilerplate when using the OcTag component. Ideally, I would love to see this baked in somehow directly into the component itself so that we don't need to always pass it as props - is there any chance to make it work like that?

Also, while testing it, I found the following:

Is that on purpose that the tag icon is missing from the badge in the resource table now?

Image

Also, the +1 is not aligned vertically.

I am not seeing the tags being coloured - I checked the theme.json response and I do not see the tags being returned there even though they are part of the theme.

Image

Comment thread web/packages/design-system/src/helpers/colors.ts Outdated
Comment thread web/packages/web-pkg/src/composables/tagColor/useTagColor.ts Outdated
Tag chips were all the same grey, so a file's tags were hard to tell apart
at a glance and the same tag looked like nothing in particular across the app.

Fill each chip with a colour derived from the tag name: `hash(tag) % length`
picks a slot in the theme's new `designTokens.tagColorsList`, emitted as
`--oc-color-tag-N`. The colour is derived, never stored, so a tag looks the
same for every user on every device with no API, storage or migration change.

The label colour is computed per fill rather than pinned to a token: the
fills come from the theme, so one token cannot stay legible against all of
them, and `--oc-color-text-inverse` gets it backwards in the light theme. The
calculation lives in `useTagColor` rather than in `OcTag` because the theme
store is a reactive dependency and `getComputedStyle` is not — resolving it
in the component would leave the label stale after a theme switch. The fill
is read straight out of `tagColorsList` rather than from the emitted css var,
since `getHexFromCssVar` reports an unresolvable var as `#000000` and would
hand a pale fill an illegible white label.

A filled chip carries `oc-tag-filled`, which forces the colours of everything
nested inside it down as `inherit`/`currentColor`. Those nested elements
otherwise repaint themselves — a `router-link` takes the global anchor colour,
a raw `OcButton` its own swatch, `.oc-tag .oc-icon > svg` a muted fill —
which left navy labels and a washed-out close icon on the deep light-theme
fills, and labels that were too light on the pale dark-theme ones. Nothing
hardcodes a colour, so both themes are covered by construction.

`OcTag` keeps its default appearance: it is the generic badge (the app-store
"most recent" pill, instance badges, the disabled-space indicator, the `+N`
counter), so both `fillColor` and `labelColor` are opt-in.

`initializeThemes` now merges with replace semantics for arrays. deepmerge
concatenates by default, which would turn a `tagColorsList` present in both
`defaults` and a theme into 60 entries and shift every hue.

Also make the tag overflow indicator on the files list a real, keyboard
reachable button that shows the hidden tags in a popover instead of opening
the sidebar. That popover is sized to its tags: `OcDrop` is a fixed 300px
wide, which left it nearly empty when it held a single chip capped at 80px by
`.resource-table-tag`, with the right alignment the tags column passes down
pushing that chip to the far edge. 300px is kept only as a cap so a long tag
still truncates.

Both `theme.json` copies gain 30 placeholder fills per theme, per theme
rather than in `defaults`, so the feature can be exercised end to end while
design's real values are outstanding. They were generated by binary-searching
HSL lightness for a target relative luminance per hue, since equal-lightness
hues differ a lot in perceived brightness: deep fills with white labels in
the light themes, pale fills with black labels in the dark ones. Worst-case
label contrast is 6.31:1 (light) and 8.52:1 (dark), higher in both
high-contrast themes. Both copies are needed — web-runtime's file is what
`pnpm build` publishes to `web/dist/themes`, while an ocis binary serves the
go:embed-ed `services/web/assets/themes` copy instead
(services/web/pkg/server/http/server.go layers it over the embedded core
assets). Every theme in a file has the same list length, so a tag keeps its
colour slot across a theme switch.

TODO: replace the placeholder palettes with the ones from design.
@gauravsoni119

gauravsoni119 commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

TBH, I am not sure whether I like the increased boilerplate when using the OcTag component. Ideally, I would love to see this baked in somehow directly into the component itself so that we don't need to always pass it as props - is there any chance to make it work like that?

Also, while testing it, I found the following:

Is that on purpose that the tag icon is missing from the badge in the resource table now?

Image Also, the +1 is not aligned vertically.

I am not seeing the tags being coloured - I checked the theme.json response and I do not see the tags being returned there even though they are part of the theme.

Image
  1. Is that on purpose that the tag icon is missing from the badge in the resource table now?
    The tag icon doesn't exist in the UI screenshot shared in the ticket.
missing-tag-icon
  1. I am not seeing the tags being coloured - I checked the theme.json response and I do not see the tags being returned there even though they are part of the theme.
    I think the WEB_ASSET_THEMES_PATH is not pointing it at the branch's themes that fixes it:

    WEB_ASSET_THEMES_PATH=/path/to/services/web/assets/themes

Could you confirm that's what you were running?

  1. the +1 is not aligned vertically.
    Fixed

  2. TBH, I am not sure whether I like the increased boilerplate when using the OcTag component
    Fixed

Collapse the two `OcTag` colour props into one. `fillColor` and `labelColor`
made every call site restate a pair that is really one decision, so the theme
store now emits a *pair* per palette entry — `--oc-color-tag-N` alongside a
derived `--oc-color-tag-N-text` — and `tagColorVarsFor(index)` maps an index
onto both. `useTagColor` is down to `tagColorIndex(name)`, and call sites pass
a single `colorIndex`. This supersedes the paragraph in the previous commit
that describes `fillColor`/`labelColor` as separately opt-in.

Derive the label colour from the theme instead of hardcoding it. `useTagColor`
held literal BLACK and WHITE constants; the pairing is now computed once per
theme in the store from the theme's own `colorPalette['text-default']` and
`['text-inverse']`, so a theme that restyles its text colours restyles its tag
labels with it.

Two properties of real theme data that this has to respect. `text-default` is
not reliably the darker of the two candidates — the light theme states
`#041e42` while the dark theme pairs `#dadcdf` with a `text-inverse` of
`#000000` — so `pickReadableTextColor` compares two unlabelled candidates and
never assumes which is which. And a theme colour is not always hex: the
shipped ownCloud light theme writes `text-default` as `oklch(13% 0.028
261.692)`, for which `hexToRgb` returns null, and an unguarded version would
have painted every label in it, dark-on-dark included. A pair that cannot be
measured falls back to plain black and white.

Normalise the hash at the source. `hashString` returned a possibly negative
int32 and left every caller to coerce it; it now returns `hash >>> 0`. This is
output-preserving — `ToInt32(h >>> 0) === ToInt32(h)`, so the `& 0x00ffffff`
in `generateHashedColorForString` is unchanged.

Align the tag overflow indicator with the cell text. `vertical-align:
text-bottom` on `.resource-table-tag-more` puts `+1` on the text's descender,
and both it and the now `inline-flex` chip wrapper are `vertical-align:
middle` so the two boxes agree.

Min label contrast across all ten shipped theme entries is 6.33:1.

TODO: replace the placeholder palettes with the ones from design.
@gauravsoni119
gauravsoni119 force-pushed the feat/ocisdev-115-coloured-tags branch from fd0f182 to 24bffe5 Compare September 9, 2026 11:40
…asured

`pickReadableTextColor` treated an unmeasurable background the same as an
unmeasurable candidate and returned the first candidate for both. The theme
store already substitutes black and white when a candidate cannot be read, so
that branch was unreachable from it; the reachable one was an unmeasurable
*fill*, which returned the theme's `text-default` unguarded.

That is the one case where the wrong answer is invisible until it ships. A fill
does not have to be hex to work: `oklch()` is valid css, so the browser paints
the chip while the contrast maths gets nothing to compare, and neither candidate
is known to be the darker one — the shipped dark theme states `text-default` as
`#dadcdf` against a `text-inverse` of `#000000`. A pale fill would have taken a
near-white label. Every placeholder palette entry is currently hex, so nothing
renders wrong today; replacing them with design's real values is what would
have exposed it.

So the function returns null when any of the three colours cannot be measured,
and the store omits that entry's `-text` property rather than emitting a colour
it could not choose. The label then inherits the surrounding text colour, which
is legible against the page by construction — the behaviour the pre-review
`tagLabelColor` had when it returned an empty string.

`tagColorVarsFor` names `currentColor` as the var's fallback to state that,
instead of relying on an undefined custom property being invalid at
computed-value time. The fallback is written without a space after the comma
because happy-dom, which the unit tests run in, discards a `var()` declaration
that has one.

The test for the old behaviour asserted "falls back to the dark candidate" but
passed only because the fixture happened to pass the dark candidate first — the
function has no concept of which is darker. Replaced by tests naming what it
actually does, plus the two cases that were missing: an unmeasurable fill emits
no label colour, and a measurable entry beside it is unaffected.
@LukasHirt

Copy link
Copy Markdown
Contributor

The tag icon doesn't exist in the UI screenshot shared in the ticket.

That mockup won't fully reflect the current state of the UI. I'm not necessarily against removing the icon from there but since that changes the current look, I would suggest not to do it in this PR.

@LukasHirt

Copy link
Copy Markdown
Contributor

I think the WEB_ASSET_THEMES_PATH is not pointing it at the branch's themes that fixes it

I did not set this env var to use the default ones - shouldn't that include the colors? I will do some more testing today.

The chips lost the `price-tag-3` icon and their rounded corners, both read off
the ticket's mockup. The mockup does not reflect the current UI, and dropping
the icon is a design change in its own right, so it does not belong in a PR
about colour: colour is now the only thing this changes about a chip.

The icon and `rounded` came out together under one spec decision and go back
together, at every site that had them — the resource table rows, the new tag
overflow popover, and both `TagsSelect` templates. The search tag filter had no
chip at all before, only an icon and a label; it keeps the chip, since that is
where the colour shows, and takes the icon back inside it.

No styling was needed for the icon on a fill: `.oc-tag.oc-tag-filled .oc-icon >
svg` already outranks `.oc-icon-passive > svg`, so the icon inherits the label
colour that goes with the fill rather than the passive swatch.

`TagsSelect.spec.ts` asserted the icon was absent, but could not have caught it
either way: `OcIcon` renders through `inline-svg`, which resolves to nothing in
happy-dom, so no icon's name reaches the rendered html and the assertion held
whether or not the icon was there. It now reads the name off the `OcIcon` prop,
and the resource table row — the one site the review actually pointed at, which
had no icon coverage — gets the same assertion.
@gauravsoni119

Copy link
Copy Markdown
Contributor Author

The tag icon doesn't exist in the UI screenshot shared in the ticket.

That mockup won't fully reflect the current state of the UI. I'm not necessarily against removing the icon from there but since that changes the current look, I would suggest not to do it in this PR.

Updated

@gauravsoni119

Copy link
Copy Markdown
Contributor Author

I think the WEB_ASSET_THEMES_PATH is not pointing it at the branch's themes that fixes it

I did not set this env var to use the default ones - shouldn't that include the colors? I will do some more testing today.

theme.json is served by the ocis web service, not the vite dev server, so a frontend from source against a pre-branch ocis image serves the old theme. WEB_ASSET_THEMES_PATH was only a shortcut to avoid the rebuild.

@LukasHirt

Copy link
Copy Markdown
Contributor

I'm running the binary directly. Not docker image. So in my setup, it should take whatever is filled in the theme.json file and serve that.

@LukasHirt

Copy link
Copy Markdown
Contributor

I did one more test and colors are correctly rendering now.

@gauravsoni119
gauravsoni119 requested a review from a team September 10, 2026 07:33
@gauravsoni119
gauravsoni119 merged commit ac8cc8b into master Sep 10, 2026
140 of 143 checks passed
@gauravsoni119
gauravsoni119 deleted the feat/ocisdev-115-coloured-tags branch September 10, 2026 08:49
<oc-drop
v-if="item.tags.length > 2"
:toggle="`#tags-overflow-${resourceDomSelector(item)}`"
mode="hover"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clicking +N hides the popover it opens.

mode="hover" maps to trigger: 'mouseenter focus' (OcDrop.vue:185) and hideOnClick: true is hardcoded (OcDrop.vue:234). tippy only bails out on a reference click when the trigger contains click (tippy.cjs.js:846), so it falls through to instance.hide() and sets didHideDueToDocumentMouseDown, which then blocks the focus trigger from reopening. Mouse users have to move the pointer off and back on.

hideOnClick isn't a prop, so this can't be fixed at the call site — it needs mode="click" or a new prop on OcDrop.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #12923

</component>
<oc-tag
v-if="item.tags.length > 2"
:id="`tags-overflow-${resourceDomSelector(item)}`"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No aria-expanded / aria-haspopup on this button, so screen readers announce an action button with no indication that a popover exists or is open.

Related: the changelog calls this "a real, keyboard reachable button", but Enter/Space does nothing — the drop is hover/focus-driven. Might be worth deciding whether activation should toggle it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #12923

size="small"
class="resource-table-tag-more"
@click="openTagsSidebar"
:aria-label="getTagsOverflowAriaLabel(item)"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WCAG 2.5.3 Label in Name: the visible text is + 3 but the accessible name is Show 3 more tags. The visible string isn't contained in the name, so speech-input users saying "click plus three" get no match.

Keeping + 3 in the name and appending the description would cover it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #12923

class="vs__deselect-lock"
name="lock"
size="small"
variation="inherit"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lock icon has no accessible-label, so OcIcon renders it aria-hidden="true". Screen-reader users never learn the tag is read-only — they just find the deselect button absent.

Since this line is already being touched: :accessible-label="$gettext('Read-only tag')".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #12923

"icon-medical": "#0984db"
}
},
"tagColorsList": [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tagColorsList is added to each theme but not to defaults.designTokens — here or in services/web/assets/themes/owncloud/theme.json, where defaults carry only breakpoints, fontSizes, sizes, spacing. Every shipped theme has 30 entries; defaults have none.

So a deployment serving its own theme.json gets no tag colours at all. Lower impact now that 2150c3d restored the icon and rounded corners (chips degrade to the previous look rather than breaking), but putting the list in defaults would let custom themes inherit a palette for free.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #12923

gauravsoni119 added a commit that referenced this pull request Sep 10, 2026
Five review comments landed on #12892 after it merged.

**The tag overflow popover dismissed itself on click.** `mode="hover"` maps to
tippy's `mouseenter focus` trigger, and `hideOnClick` is hardcoded true in
`OcDrop`. tippy only skips its document-press hide when the trigger contains
`click`, so a click on the button fell through to `hide()` and set
`didHideDueToDocumentMouseDown`, which then blocked the focus trigger from
reopening it — the pointer had to leave and come back.

`mode="click"` fixes it without touching the design system: tippy toggles a
click-triggered instance, so the button opens and closes it, Enter/Space work
because a native button activation dispatches a click, and Escape still closes
via the `hideOnEsc` plugin with a click able to reopen it. Under `hover` that
last one was a dead end — Escape hid the popover and no new focus event could
follow. The trade is that hovering no longer reveals the tags; a disclosure
that answers to the keyboard is worth more than a hover shortcut.

That click also had to stop propagating: the button sits inside the `<tr>` that
`OcTable` highlights on click, so opening the popover selected the file as
well. tippy's listener is on the same element, so stopping propagation still
toggles the popover.

`is-nested` goes with it. It was set purely to suppress the `hideAll` that ran
when a pointer crossed the button, and pointer movement no longer opens
anything. Dropping it also un-silences `showDrop`/`hideDrop`, which `OcDrop`
only emits for drops that are not nested.

**The button claimed to be a popover trigger without saying so.** It now
carries `aria-haspopup` and an `aria-expanded` driven by those two events.
tippy manages `aria-expanded` itself only when the reference does not already
have one (`hasAriaExpanded` is read once at create time), and Vue renders the
attribute before `OcDrop` mounts, so there is one owner, not two.

**The accessible name dropped the visible text.** WCAG 2.5.3 Label in Name
wants the visible `+ 3` inside the name; `Show 3 more tags` left speech input
with nothing to match. The count now leads the string.

**The read-only tag lock was invisible to screen readers.** No
`accessible-label` means `OcIcon` renders it `aria-hidden`, so the only signal
that a tag cannot be removed was the absent deselect button.

**Themes inherited no tag palette.** `tagColorsList` was missing from
`defaults.designTokens`, so a deployment serving its own theme.json gets grey
chips. Seeding the defaults with the light palette turned out to be the wrong
place for it: every other value in that block is an empty-string placeholder,
because supplying colours is a theme's responsibility. The defaults stay
colourless. `useTagColor` already returns -1 without a list and `OcTag` then
renders the plain chip, which is what tags looked like before this feature.
gauravsoni119 added a commit that referenced this pull request Sep 10, 2026
Five review comments landed on #12892 after it merged.

**The tag overflow popover dismissed itself on click.** `mode="hover"` maps to
tippy's `mouseenter focus` trigger, and `hideOnClick` is hardcoded true in
`OcDrop`. tippy only skips its document-press hide when the trigger contains
`click`, so a click on the button fell through to `hide()` and set
`didHideDueToDocumentMouseDown`, which then blocked the focus trigger from
reopening it — the pointer had to leave and come back.

`mode="click"` fixes it without touching the design system: tippy toggles a
click-triggered instance, so the button opens and closes it, Enter/Space work
because a native button activation dispatches a click, and Escape still closes
via the `hideOnEsc` plugin with a click able to reopen it. Under `hover` that
last one was a dead end — Escape hid the popover and no new focus event could
follow. The trade is that hovering no longer reveals the tags; a disclosure
that answers to the keyboard is worth more than a hover shortcut.

That click also had to stop propagating: the button sits inside the `<tr>` that
`OcTable` highlights on click, so opening the popover selected the file as
well. tippy's listener is on the same element, so stopping propagation still
toggles the popover.

`is-nested` goes with it. It was set purely to suppress the `hideAll` that ran
when a pointer crossed the button, and pointer movement no longer opens
anything. Dropping it also un-silences `showDrop`/`hideDrop`, which `OcDrop`
only emits for drops that are not nested.

**The button claimed to be a popover trigger without saying so.** It now
carries `aria-haspopup` and an `aria-expanded` driven by those two events.
tippy manages `aria-expanded` itself only when the reference does not already
have one (`hasAriaExpanded` is read once at create time), and Vue renders the
attribute before `OcDrop` mounts, so there is one owner, not two.

**The accessible name dropped the visible text.** WCAG 2.5.3 Label in Name
wants the visible `+ 3` inside the name; `Show 3 more tags` left speech input
with nothing to match. The count now leads the string.

**The read-only tag lock was invisible to screen readers.** No
`accessible-label` means `OcIcon` renders it `aria-hidden`, so the only signal
that a tag cannot be removed was the absent deselect button.

**Themes inherited no tag palette.** `tagColorsList` was missing from
`defaults.designTokens`, so a deployment serving its own theme.json gets grey
chips. Seeding the defaults with the light palette turned out to be the wrong
place for it: every other value in that block is an empty-string placeholder,
because supplying colours is a theme's responsibility. The defaults stay
colourless. `useTagColor` already returns -1 without a list and `OcTag` then
renders the plain chip, which is what tags looked like before this feature.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants