diff --git a/RELEASE.md b/RELEASE.md index 92188238..150f2d22 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,15 +1,10 @@ -> v0.4.1 ~ "Resource identity: pills, one-line identity cells, hover summaries and select options" +> v0.4.2 ~ "Restore the hidden state of tooltips and popovers in production builds" --- ## Highlights -- **Resource descriptor registry** — `utils/resource-registry` stores one descriptor per kind of record (title, identifier, image, status, badges, facts, how it opens) in the shared `universe/registry-service`, so the host and every engine read one set. `resolveResourceKey` accepts a record, an identity stub, a model name or alias, a `prefix:key` string, a PHP class name or a polymorphic type; `openResource` unwraps proxies and stubs, swaps a subtype for its canonical record and never throws. The `resource-type`, `resource-component` and `resource-relation` helpers and the `resource-registry` service expose it to templates and engines. -- **Resource::Pill, Resource::Summary, Resource::HoverCard** — a descriptor-driven pill that opens its record and shows a compact summary card on hover (photo or icon, name and identifier, status and online state, a handful of facts and one View action). The card is lazy: nothing renders until the pointer has rested on the target, and it hydrates only then. -- **Table::Cell::Identity** — a one-line identity cell: a small image with the status dot, the name and up to two inline badges, no status badge. It opens the record on click, disables on `column.permission`, and never fires the row action as well. -- **Resource::SelectOption** — the option row for record pickers, with a compact variant for the closed trigger. `filter/model` renders options through it and the new `filter/model-multiple` filter selects several records. -- **Core resource families** — pills, summaries, select options and identity cells for user, company, group, role, file and category, registered by an instance-initializer in the host. -- **Fixes** — `Attach::Popover` applies `@class`, keeps an interactive popover enterable while shown, follows later changes to `@isShown` and removes its listeners on destroy; `Attach::Tooltip` forwards `hideDelay`, `hideDuration`, `@class`, `@style`, `floatingTarget`, `floatingContainer` and `onChange`; `Pill` renders a static span when it has nothing to do and hands its tooltip component the resource; `MultiSelect` forwards `@selectedItemComponent`, `@searchField` and `@searchPlaceholder`; `table/cell/resource-identity` no longer renders an empty meta row. -- **Dummy app** — `@fleetbase/ember-core` is declared as a dependency so the components that import it can render in tests. +- **Fix: every tooltip and popover rendered visible in production** — v0.4.1 started forwarding the host's browser targets to `postcss-preset-env`, which then skipped flattening CSS nesting for browsers that support it natively. The attacher styles that hold an `Attach::Tooltip` or `Attach::Popover` at `opacity: 0` until it is shown are written nested, and ember-cli's production minifier (clean-css) cannot parse nesting: it emitted those rules as top-level `& > …` selectors that match nothing, so every attachment appeared at full opacity without a hover. Development builds were unaffected because the browser parsed the nested block itself. The preset now always flattens nesting, whatever the targets. +- **Regression test** — `tests/node/postcss-build-test.cjs` compiles the attacher styles for the console's targets, asserts the flat hide rule is present, and minifies the result with clean-css to prove it survives a production build. --- ## Need help? diff --git a/lib/postcss-options.js b/lib/postcss-options.js index 54700538..2fcaa2c5 100644 --- a/lib/postcss-options.js +++ b/lib/postcss-options.js @@ -28,8 +28,12 @@ module.exports = function buildPostcssOptions(browsers) { }), postcssMixins, // Ember CLI's targets are not automatically forwarded to an - // instantiated preset. Avoid polyfilling selectors the host supports. - postcssPresetEnv({ stage: 1, browsers, autoprefixer: false }), + // instantiated preset. Avoid polyfilling selectors the host supports, + // but always flatten nesting: the addon styles nest freely, and the + // production minifier (clean-css) cannot parse native nesting, so an + // unflattened block loses every nested rule, including the ones + // that hide a tooltip until it is shown. + postcssPresetEnv({ stage: 1, browsers, autoprefixer: false, features: { 'nesting-rules': true } }), postcssEach, tailwind(tailwindConfigPath), autoprefixer({ overrideBrowserslist: browsers }), diff --git a/package.json b/package.json index 56736976..4b5f2481 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fleetbase/ember-ui", - "version": "0.4.1", + "version": "0.4.2", "description": "Fleetbase UI provides all the interface components, helpers, services and utilities for building a Fleetbase extension into the Console.", "keywords": [ "fleetbase-ui", diff --git a/tests/node/postcss-build-test.cjs b/tests/node/postcss-build-test.cjs index 3d20e185..7d2ef206 100644 --- a/tests/node/postcss-build-test.cjs +++ b/tests/node/postcss-build-test.cjs @@ -83,3 +83,21 @@ test('JavaScript utility changes invalidate the Broccoli CSS cache', async () => fs.rmSync(directory, { recursive: true, force: true }); } }); + +test('tooltip hide rules are flattened for modern targets and survive the production minifier', async () => { + // postcss-preset-env skips nesting for browsers that support it natively, but + // ember-cli's clean-css minifier cannot parse nesting, and a nested attacher + // block loses every rule that hides a tooltip until it is shown. + const from = path.resolve(__dirname, '../../addon/styles/components/attacher.css'); + const { filtered } = await compile(fs.readFileSync(from, 'utf8'), from); + assert.doesNotMatch(filtered.css, /&/); + assert.match(filtered.css, /\.ember-attacher\[x-placement\^=['"]?top['"]?\]\s*>\s*\.ember-attacher-fill\s*\{[^}]*opacity:\s*0/); + + const emberCli = path.dirname(require.resolve('ember-cli/package.json')); + const CleanCSS = require(require.resolve('clean-css', { paths: [emberCli] })); + const minified = new CleanCSS({}).minify(filtered.css); + assert.deepEqual(minified.errors, []); + assert.deepEqual(minified.warnings, []); + assert.match(minified.styles, /\.ember-attacher\[x-placement\^=['"]?top['"]?\]>\.ember-attacher-fill\{[^}]*opacity:0/); + assert.doesNotMatch(minified.styles, /\}&/); +});