Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions js/src/combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,13 @@ class Combobox extends BaseComponent {
showTarget.classList.add(CLASS_NAME_SHOW)
this._getAriaExpandedTarget().setAttribute('aria-expanded', 'true')

// The panel carries its own open state, teleported or not: `.popup` keys
// both its display and its entry transition on it, so a panel shown only
// through an ancestor's class would be laid out and never fade in.
this._menu.classList.add(CLASS_NAME_SHOW)

if (this._config.container) {
this._menu.style.minWidth = `${showTarget.offsetWidth}px`
this._menu.classList.add(CLASS_NAME_SHOW)
}

EventHandler.trigger(this._element, this.constructor.eventName('shown'))
Expand All @@ -112,10 +116,7 @@ class Combobox extends BaseComponent {

this._getShowTarget().classList.remove(CLASS_NAME_SHOW)
this._getAriaExpandedTarget().setAttribute('aria-expanded', 'false')

if (this._config.container) {
this._menu.classList.remove(CLASS_NAME_SHOW)
}
this._menu.classList.remove(CLASS_NAME_SHOW)

this._onHideEnd()
EventHandler.trigger(this._element, this.constructor.eventName('hidden'))
Expand Down
16 changes: 16 additions & 0 deletions js/tests/unit/autocomplete.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,22 @@ describe('Autocomplete', () => {
})

describe('show', () => {
// The panel keys its own display and entry transition on this class, so a
// panel opened only through an ancestor's class is laid out and invisible.
it('should mark the popup itself as shown', () => {
fixtureEl.innerHTML = '<div></div>'
const element = fixtureEl.querySelector('div')
const instance = new Autocomplete(element, { options: [{ value: 1, label: 'One' }] })

instance.show()

expect(instance._menu.classList.contains('show')).toBe(true)

instance.hide()

expect(instance._menu.classList.contains('show')).toBe(false)
})

it('should show the autocomplete dropdown', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<div class="autocomplete"></div>'
Expand Down
16 changes: 16 additions & 0 deletions js/tests/unit/multi-select.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,22 @@ describe('MultiSelect', () => {
})

describe('show', () => {
// The panel keys its own display and entry transition on this class, so a
// panel opened only through an ancestor's class is laid out and invisible.
it('should mark the popup itself as shown', () => {
fixtureEl.innerHTML = '<select></select>'
const element = fixtureEl.querySelector('select')
const instance = new MultiSelect(element, { options: [] })

instance.show()

expect(instance._menu.classList.contains('show')).toBe(true)

instance.hide()

expect(instance._menu.classList.contains('show')).toBe(false)
})

it('should show the multi select dropdown', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<select></select>'
Expand Down
30 changes: 25 additions & 5 deletions js/tests/visual/field-components.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,28 @@ const mount = html => {

// The matcher appends the browser and platform to the baseline name itself
// (`<name>-chromium-linux.png`), so each platform compares against its own set.
const shoot = (element, name) =>
expect(page.elementLocator(element)).toMatchScreenshot(name, screenshotOptions)
// Two frames before capturing. Transitions are frozen, but an element entering
// through `@starting-style` still renders its start state on the frame it is
// shown — one paint later it is settled, and the screenshot is of the state
// the component actually rests in.
const settle = () => new Promise(resolve => {
requestAnimationFrame(() => requestAnimationFrame(resolve))
})

// A panel that fades in is composited, and a composited layer rasterises text
// slightly differently from one frame to the next — enough to trip a
// comparator that counts antialiased pixels. `tolerant` raises the allowance
// for those, and only those: the icon-colour changes this suite exists to
// catch move far more pixels than this.
const shoot = async (element, name, { tolerant = false } = {}) => {
await settle()

const options = tolerant ?
{ comparatorOptions: { ...screenshotOptions.comparatorOptions, allowedMismatchedPixels: 2500 } } :
screenshotOptions

return expect(page.elementLocator(element)).toMatchScreenshot(name, options)
}

const frame = () => container.querySelector('.form-control-group')
const popup = () => container.querySelector('.popup')
Expand Down Expand Up @@ -138,7 +158,7 @@ describe('date picker', () => {
it('open popup', async () => {
const dp = new DatePicker(mount(), { locale: 'en-US', date: DATE })
dp.show()
await shoot(popup(), 'date-picker-popup')
await shoot(popup(), 'date-picker-popup', { tolerant: true })
dp.dispose()
})

Expand All @@ -153,7 +173,7 @@ describe('date picker', () => {
document.documentElement.dataset.coreuiTheme = 'dark'
const dp = new DatePicker(mount(), { locale: 'en-US', date: DATE })
dp.show()
await shoot(popup(), 'date-picker-popup-dark')
await shoot(popup(), 'date-picker-popup-dark', { tolerant: true })
dp.dispose()
})
})
Expand Down Expand Up @@ -225,7 +245,7 @@ describe('autocomplete', () => {
it('open popup', async () => {
const ac = new Autocomplete(mount(), { options: OPTIONS, indicator: true })
ac.show()
await shoot(popup(), 'autocomplete-popup')
await shoot(popup(), 'autocomplete-popup', { tolerant: true })
ac.dispose()
})
})
Expand Down
5 changes: 0 additions & 5 deletions scss/_combobox.scss
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,6 @@ $combobox-tokens: defaults(
@include tokens($combobox-tokens);

min-width: var(--#{$prefix}combobox-popup-min-width);

.autocomplete.show &,
.form-multi-select.show & {
display: block;
}
}

.combobox-header {
Expand Down
4 changes: 0 additions & 4 deletions scss/_date-picker.scss
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,6 @@ $date-picker-tokens: defaults(
// left here is the panel's own shape and the parent-driven open state.
.date-picker-dropdown {
width: min-content;

.date-picker.show & {
display: block;
}
}

.date-picker-body {
Expand Down
28 changes: 28 additions & 0 deletions scss/_popup.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
@use "mixins/border-radius" as *;
@use "mixins/box-shadow" as *;
@use "mixins/tokens" as *;
@use "mixins/transition" as *;
@use "config" as *;

// scss-docs-start popup-variables
Expand All @@ -11,6 +12,8 @@ $popup-border-width: var(--#{$prefix}border-width) !default;
$popup-border-color: var(--#{$prefix}border-color) !default;
$popup-border-radius: var(--#{$prefix}border-radius) !default;
$popup-box-shadow: var(--#{$prefix}box-shadow) !default;
$popup-transition-duration: .15s !default;
$popup-transition-timing: cubic-bezier(.22, 1, .36, 1) !default;
// scss-docs-end popup-variables

// scss-docs-start popup-tokens
Expand All @@ -24,6 +27,8 @@ $popup-tokens: defaults(
--#{$prefix}popup-border-color: #{$popup-border-color},
--#{$prefix}popup-border-radius: #{$popup-border-radius},
--#{$prefix}popup-box-shadow: #{$popup-box-shadow},
--#{$prefix}popup-transition-duration: #{$popup-transition-duration},
--#{$prefix}popup-transition-timing: #{$popup-transition-timing},
),
$popup-tokens
);
Expand All @@ -46,10 +51,33 @@ $popup-tokens: defaults(
background-clip: padding-box;
border: var(--#{$prefix}popup-border-width) solid var(--#{$prefix}popup-border-color);
@include border-radius(var(--#{$prefix}popup-border-radius));
opacity: 0;
@include box-shadow(var(--#{$prefix}popup-box-shadow));

// `display` is what makes a popup appear, and it cannot be interpolated,
// so `allow-discrete` lets it flip at the right end of the transition while
// opacity and transform carry the motion. `@starting-style` supplies the
// state to animate FROM: the element does not exist to be styled before it
// is shown, so the base rule cannot express it.
//
// Opacity only, no scale. A popup carries no placement attribute, so a
// scale would need an origin we cannot know — and on a panel the size of a
// calendar even a 2% scale reads as the panel jumping rather than
// appearing. The combobox list hides it; the pickers do not.
@include transition(
opacity var(--#{$prefix}popup-transition-duration) var(--#{$prefix}popup-transition-timing),
display var(--#{$prefix}popup-transition-duration) allow-discrete
);

&.show {
display: block;
opacity: 1;
}
}

@starting-style {
.popup.show {
opacity: 0;
}
}
}
4 changes: 0 additions & 4 deletions scss/_time-picker.scss
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,6 @@ $time-picker-tokens: defaults(
// open state here.
.time-picker-dropdown {
width: min-content;

.time-picker.show & {
display: block;
}
}

.time-picker-body {
Expand Down
Loading