Skip to content

Commit fb81eb5

Browse files
authored
Merge pull request #2530 from hashicorp/alex-ju/dropdown-match-trigger-width
`Dropdown` - add `matchToggleWidth`
2 parents 33986fa + 130453c commit fb81eb5

File tree

11 files changed

+172
-64
lines changed

11 files changed

+172
-64
lines changed

.changeset/lucky-fireants-cover.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hashicorp/design-system-components": minor
3+
---
4+
5+
`Dropdown` - added `@matchToggleWidth` argument

packages/components/src/components/hds/dropdown/index.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export interface HdsDropdownSignature {
4545
width?: string;
4646
enableCollisionDetection?: FloatingUIOptions['enableCollisionDetection'];
4747
preserveContentInDom?: boolean;
48+
matchToggleWidth?: boolean;
4849
};
4950
Blocks: {
5051
default: [
@@ -93,17 +94,23 @@ export default class HdsDropdown extends Component<HdsDropdownSignature> {
9394
return this.args.enableCollisionDetection ?? false;
9495
}
9596

97+
get matchToggleWidth(): FloatingUIOptions['matchToggleWidth'] {
98+
return this.args.matchToggleWidth ?? false;
99+
}
100+
96101
get anchoredPositionOptions(): {
97102
placement: FloatingUIOptions['placement'];
98103
offsetOptions: FloatingUIOptions['offsetOptions'];
99104
enableCollisionDetection: FloatingUIOptions['enableCollisionDetection'];
105+
matchToggleWidth: FloatingUIOptions['matchToggleWidth'];
100106
} {
101107
// custom options specific for the `RichTooltip` component
102108
// for details see the `hds-anchored-position` modifier
103109
return {
104110
placement: HdsDropdownPositionToPlacementValues[this.listPosition],
105111
offsetOptions: 4,
106112
enableCollisionDetection: this.enableCollisionDetection ? 'flip' : false,
113+
matchToggleWidth: this.matchToggleWidth,
107114
};
108115
}
109116

@@ -136,8 +143,8 @@ export default class HdsDropdown extends Component<HdsDropdownSignature> {
136143
// context: https://github.com/hashicorp/design-system/pull/2309#discussion_r1706941892
137144
classes.push(`hds-dropdown__content--position-${this.listPosition}`);
138145

139-
// add a class based on the @width argument
140-
if (this.args.width) {
146+
// add a class based on the @width or @matchToggleWidth arguments
147+
if (this.args.width || this.args.matchToggleWidth) {
141148
classes.push('hds-dropdown__content--fixed-width');
142149
}
143150

packages/components/src/modifiers/hds-anchored-position.ts

+14-20
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,7 @@ import {
2020
// see: https://floating-ui.com/docs/hide
2121
// hide,
2222
// ---
23-
// this could be used in the future if we want to give consumers an option to:
24-
// - let the "floating" element auto-resize when there is not enough space (usually vertical) in the viewport to contain the entire "floating" element
25-
// - let the "floating" element match the width of the "trigger" (it may have min/max width/heigh via CSS too)
26-
// see: https://floating-ui.com/docs/size
27-
// notice: below you can find a preliminary code implementation that was tested and worked relatively well
28-
// size,
29-
// ---
23+
size,
3024
} from '@floating-ui/dom';
3125

3226
import type {
@@ -72,6 +66,7 @@ export type FloatingUIOptions = {
7266
enableCollisionDetection?: boolean | 'shift' | 'flip' | 'auto';
7367
arrowElement?: ArrowOptions['element'];
7468
arrowPadding?: ArrowOptions['padding'];
69+
matchToggleWidth?: boolean;
7570
};
7671

7772
// we use this function to process all the options provided to the modifier in a single place,
@@ -94,6 +89,7 @@ export const getFloatingUIOptions = (
9489
enableCollisionDetection,
9590
arrowElement,
9691
arrowPadding,
92+
matchToggleWidth,
9793
} = options;
9894

9995
// we build dynamically the list of middleware functions to invoke, depending on the options provided
@@ -132,20 +128,18 @@ export const getFloatingUIOptions = (
132128
);
133129
}
134130

135-
// TODO? commenting this for now, will need to make this conditional to some argument (and understand how this relates to the `@height` argument)
136131
// https://floating-ui.com/docs/size#match-reference-width
137-
// size({
138-
// apply({ rects, elements }) {
139-
// Object.assign(elements.floating.style, {
140-
// width: `${rects.reference.width}px`,
141-
// });
142-
// },
143-
// });
144-
// size({
145-
// apply: ({ availableWidth, availableHeight, middlewareData }) => {
146-
// middlewareData.size = { availableWidth, availableHeight };
147-
// },
148-
// }),
132+
if (matchToggleWidth) {
133+
middleware.push(
134+
size({
135+
apply({ rects, elements }) {
136+
Object.assign(elements.floating.style, {
137+
width: `${rects.reference.width}px`,
138+
});
139+
},
140+
})
141+
);
142+
}
149143

150144
middleware.push(...middlewareExtra);
151145

showcase/app/styles/showcase-pages/dropdown.scss

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ body.components-dropdown {
2323

2424
.shw-component-dropdown-fixed-height-container {
2525
min-height: 200px;
26+
outline: 1px dotted var(--shw-color-gray-400);
2627
}
2728

2829
.shw-component-dropdown-states-matrix {

showcase/app/templates/components/dropdown.hbs

+92-37
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,99 @@
1313

1414
<Shw::Grid @columns={{2}} as |SG|>
1515
{{#each @model.POSITIONS as |position|}}
16-
<SG.Item {{style padding="5em"}} @label={{capitalize position}}>
17-
<Hds::Dropdown @isOpen={{true}} @listPosition={{position}} as |D|>
18-
<D.ToggleButton @color="secondary" @text="Menu" />
19-
<D.Interactive @href="#">Create</D.Interactive>
20-
<D.Interactive @href="#">Edit</D.Interactive>
21-
</Hds::Dropdown>
16+
<SG.Item @label={{capitalize position}}>
17+
<Shw::Outliner {{style padding="6em 12em"}}>
18+
<Hds::Dropdown @isOpen={{true}} @listPosition={{position}} as |D|>
19+
<D.ToggleButton @color="secondary" @text="Menu" />
20+
<D.Interactive @href="#">Create</D.Interactive>
21+
<D.Interactive @href="#">Edit</D.Interactive>
22+
</Hds::Dropdown>
23+
</Shw::Outliner>
2224
</SG.Item>
2325
{{/each}}
2426
</Shw::Grid>
2527

2628
<Shw::Divider @level={{2}} />
2729

30+
<Shw::Text::H2>Width</Shw::Text::H2>
31+
32+
<Shw::Grid @columns={{4}} @gap="2rem" as |SG|>
33+
{{#let (array false true) as |options|}}
34+
{{#each options as |option|}}
35+
<SG.Item @label="matchToggleWidth={{option}}" as |SGI|>
36+
<SGI.Label><code>ToggleButton</code> auto</SGI.Label>
37+
<div class="shw-component-dropdown-fixed-height-container">
38+
<Hds::Dropdown @isOpen={{true}} @listPosition="bottom-left" @matchToggleWidth={{option}} as |D|>
39+
<D.ToggleButton @color="secondary" @text="Menu" />
40+
<D.Interactive @href="#">
41+
Lorem ipsum dolor sit amet
42+
</D.Interactive>
43+
<D.Interactive @href="#">
44+
Consectetur adipisicing elit
45+
</D.Interactive>
46+
</Hds::Dropdown>
47+
</div>
48+
</SG.Item>
49+
<SG.Item @label="matchToggleWidth={{option}}" as |SGI|>
50+
<SGI.Label><code>ToggleButton</code> 100%</SGI.Label>
51+
<div class="shw-component-dropdown-fixed-height-container">
52+
<Hds::Dropdown @isOpen={{true}} @listPosition="bottom-left" @matchToggleWidth={{option}} as |D|>
53+
<D.ToggleButton {{style width="100%"}} @color="secondary" @text="Menu" />
54+
<D.Interactive @href="#">
55+
Lorem ipsum dolor sit amet
56+
</D.Interactive>
57+
<D.Interactive @href="#">
58+
Consectetur adipisicing elit
59+
</D.Interactive>
60+
</Hds::Dropdown>
61+
</div>
62+
</SG.Item>
63+
<SG.Item @label="matchToggleWidth={{option}}" as |SGI|>
64+
<SGI.Label><code>ToggleButton</code> auto + <code>@width="200px"</code></SGI.Label>
65+
<div class="shw-component-dropdown-fixed-height-container">
66+
<Hds::Dropdown
67+
@isOpen={{true}}
68+
@listPosition="bottom-left"
69+
@width="200px"
70+
@matchToggleWidth={{option}}
71+
as |D|
72+
>
73+
<D.ToggleButton @color="secondary" @text="Menu" />
74+
<D.Interactive @href="#">
75+
Lorem ipsum dolor sit amet
76+
</D.Interactive>
77+
<D.Interactive @href="#">
78+
Consectetur adipisicing elit
79+
</D.Interactive>
80+
</Hds::Dropdown>
81+
</div>
82+
</SG.Item>
83+
<SG.Item @label="matchToggleWidth={{option}}" as |SGI|>
84+
<SGI.Label><code>ToggleButton</code> auto + <code>@width="100%"</code></SGI.Label>
85+
<div class="shw-component-dropdown-fixed-height-container">
86+
<Hds::Dropdown
87+
@isOpen={{true}}
88+
@listPosition="bottom-left"
89+
@width="100%"
90+
@matchToggleWidth={{option}}
91+
as |D|
92+
>
93+
<D.ToggleButton @color="secondary" @text="Menu" />
94+
<D.Interactive @href="#">
95+
Lorem ipsum dolor sit amet
96+
</D.Interactive>
97+
<D.Interactive @href="#">
98+
Consectetur adipisicing elit
99+
</D.Interactive>
100+
</Hds::Dropdown>
101+
</div>
102+
</SG.Item>
103+
{{/each}}
104+
{{/let}}
105+
</Shw::Grid>
106+
107+
<Shw::Divider @level={{2}} />
108+
28109
<Shw::Text::H2>Display</Shw::Text::H2>
29110

30111
<Shw::Flex as |SF|>
@@ -1525,35 +1606,9 @@
15251606
<section>
15261607
<Shw::Text::H2>Demo</Shw::Text::H2>
15271608

1528-
<Shw::Grid @columns={{2}} @gap="2rem" as |SG|>
1529-
<SG.Item @label="With fixed width">
1530-
<div class="shw-component-dropdown-fixed-height-container">
1531-
<Hds::Dropdown @listPosition="bottom-left" @width="200px" as |D|>
1532-
<D.ToggleButton @color="secondary" @text="Menu" />
1533-
<D.Interactive @href="#">
1534-
Lorem ipsum dolor sit amet
1535-
</D.Interactive>
1536-
<D.Interactive @href="#">
1537-
Consectetur adipisicing elit
1538-
</D.Interactive>
1539-
</Hds::Dropdown>
1540-
</div>
1541-
</SG.Item>
1542-
<SG.Item @label="With 100% width">
1543-
<div class="shw-component-dropdown-fixed-height-container">
1544-
<Hds::Dropdown @listPosition="bottom-left" @width="100%" as |D|>
1545-
<D.ToggleButton @color="secondary" @text="Menu" />
1546-
<D.Interactive @href="#">
1547-
Lorem ipsum dolor sit amet
1548-
</D.Interactive>
1549-
<D.Interactive @href="#">
1550-
Consectetur adipisicing elit
1551-
</D.Interactive>
1552-
</Hds::Dropdown>
1553-
</div>
1554-
</SG.Item>
1555-
<SG.Item as |SG|>
1556-
<SG.Label>Using <code>preserveContentInDom</code> argument</SG.Label>
1609+
<Shw::Flex as |SF|>
1610+
<SF.Label>Using <code>preserveContentInDom</code></SF.Label>
1611+
<SF.Item>
15571612
<div class="shw-component-dropdown-fixed-height-container">
15581613
<Hds::Dropdown @listPosition="bottom-left" @preserveContentInDom={{true}} as |D|>
15591614
<D.ToggleButton @color="secondary" @text="Menu" />
@@ -1568,7 +1623,7 @@
15681623
</D.Footer>
15691624
</Hds::Dropdown>
15701625
</div>
1571-
</SG.Item>
1572-
</Shw::Grid>
1626+
</SF.Item>
1627+
</Shw::Flex>
15731628

15741629
</section>

showcase/tests/integration/components/hds/dropdown/index-test.js

+18-2
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
* SPDX-License-Identifier: MPL-2.0
44
*/
55

6-
import { module, test } from 'qunit';
6+
import { module, test, skip } from 'qunit';
77
import { setupRenderingTest } from 'ember-qunit';
88
import { render, click } from '@ember/test-helpers';
99
import { hbs } from 'ember-cli-htmlbars';
1010

11+
import { wait } from 'showcase/tests/helpers';
12+
1113
module('Integration | Component | hds/dropdown/index', function (hooks) {
1214
setupRenderingTest(hooks);
1315

@@ -167,7 +169,21 @@ module('Integration | Component | hds/dropdown/index', function (hooks) {
167169
</Hds::Dropdown>
168170
`);
169171
await click('button#test-toggle-button');
170-
assert.dom('#test-dropdown ul').hasStyle({ width: '248px' });
172+
assert.dom('.hds-dropdown__content').hasStyle({ width: '248px' });
173+
});
174+
175+
// flaky erroring with 'ResizeObserver loop completed with undelivered notifications.'
176+
skip('it should render the content with the same width as the ToggleButton if @matchToggleWidth is set', async function (assert) {
177+
await render(hbs`
178+
<Hds::Dropdown id="test-dropdown" @matchToggleWidth={{true}} as |D|>
179+
<D.ToggleButton {{style width="200px"}} @text="toggle button" id="test-toggle-button" />
180+
<D.Interactive @route="components.dropdown" @text="interactive" />
181+
</Hds::Dropdown>
182+
`);
183+
await click('button#test-toggle-button');
184+
await wait();
185+
// the expected value is 200px, but the ember-testing frame has a `transform: scale(0.5);`
186+
assert.dom('.hds-dropdown__content').hasStyle({ width: '100px' });
171187
});
172188

173189
// PRESERVE DISCLOSED CONTENT WHEN INTERACTED WITH

showcase/tests/integration/modifiers/hds-anchored-position-test.js

+3
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ module('Integration | Modifier | hds-anchored-position', function (hooks) {
239239
assert.deepEqual(floatingStyle.position, 'absolute');
240240
assert.deepEqual(floatingStyle.top, '40px');
241241
assert.deepEqual(floatingStyle.left, '-50px');
242+
assert.deepEqual(floatingStyle.width, '200px');
242243
assert.deepEqual(arrowStyle.left, '95px');
243244
assert.deepEqual(
244245
this.arrowElement.getAttribute('data-hds-anchored-arrow-placement'),
@@ -261,6 +262,7 @@ module('Integration | Modifier | hds-anchored-position', function (hooks) {
261262
strategy: 'fixed',
262263
offsetOptions: 20,
263264
arrowElement: this.arrowElement,
265+
matchToggleWidth: true,
264266
};
265267
// apply the modifier to the elements (after the rendering)
266268
await anchoredElementModifier(
@@ -275,6 +277,7 @@ module('Integration | Modifier | hds-anchored-position', function (hooks) {
275277
assert.deepEqual(floatingStyle.position, 'fixed');
276278
assert.deepEqual(floatingStyle.top, '70px');
277279
assert.deepEqual(floatingStyle.left, '10px');
280+
assert.deepEqual(floatingStyle.width, '100px');
278281
assert.deepEqual(arrowStyle.left, '45px');
279282
assert.deepEqual(
280283
this.arrowElement.getAttribute('data-hds-anchored-arrow-placement'),

website/docs/components/dropdown/index.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ previewImage: assets/illustrations/components/dropdown.jpg
1010
navigation:
1111
keywords: ['select', 'menu', 'action menu', 'list']
1212
status:
13-
updated: 4.13.0
13+
updated: 4.14.0
1414
---
1515

1616
<section data-tab="Guidelines">
@@ -33,6 +33,7 @@ status:
3333
</section>
3434

3535
<section data-tab="Version history">
36+
@include "partials/version-history/4.14.0.md"
3637
@include "partials/version-history/4.13.0.md"
3738
@include "partials/version-history/4.12.0.md"
3839
@include "partials/version-history/4.10.0.md"

website/docs/components/dropdown/partials/code/component-api.md

+5
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,15 @@ The Dropdown component is composed of different child components each with their
7575
</C.Property>
7676
<C.Property @name="width" @type="string" @valueNote="any valid CSS width (px, rem, etc)">
7777
By default, the Dropdown List has a `min-width` of `200px` and a `max-width` of `400px`, so it adapts to the content size. If a `@width` parameter is provided then the list will have a fixed width.
78+
<br/><br/>We discourage the use of percentage values for this argument. The Dropdown list is [a `popover` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/popover), so relative units use `document` as a reference (not the parent element), meaning percentage values act similar to `vw`.
79+
If `@matchToggleWidth` is set, `@width` is overridden.
7880
</C.Property>
7981
<C.Property @name="height" @type="string" @valueNote="any valid CSS height (px, rem, etc)">
8082
If a `@height` parameter is provided then the list will have a max-height.
8183
</C.Property>
84+
<C.Property @name="matchToggleWidth" @type="boolean" @default="false">
85+
Sets the Dropdown List’s width to match the width of the Toggle. It overrides the `@width` value if set.
86+
</C.Property>
8287
<C.Property @name="preserveContentInDom" @type="boolean" @default="false">
8388
Controls if the content is always rendered in the DOM, even when the Dropdown is closed.
8489
</C.Property>

0 commit comments

Comments
 (0)