Skip to content

Commit 6b8c687

Browse files
authored
Merge pull request #2309 from hashicorp/alex-ju/dropdown-popver
Replace `MenuPrimitive` with `PopoverPrimitive` in `Dropdown` and `Breadcrumb::Truncation`
2 parents 519ffa8 + 41aa18f commit 6b8c687

File tree

25 files changed

+260
-136
lines changed

25 files changed

+260
-136
lines changed

.changeset/olive-mugs-travel.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@hashicorp/design-system-components": minor
3+
---
4+
5+
`Dropdown` - added `@enableCollisionDetection` and `@isOpen` arguments
6+
7+
`Dropdown`, `Breadcrumb::Truncation` - replaced `MenuPrimitive` with `PopoverPrimitive`
8+
9+
`MenuPrimitive` - marked as deprecated and will be removed in the next major version

packages/components/src/components/hds/breadcrumb/truncation.hbs

+10-9
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,25 @@
33
SPDX-License-Identifier: MPL-2.0
44
}}
55
<li class="hds-breadcrumb__item hds-breadcrumb__item--is-truncation" ...attributes>
6-
<Hds::MenuPrimitive>
7-
<:toggle as |t|>
6+
<Hds::PopoverPrimitive @enableClickEvents={{true}} as |PP|>
7+
<div {{PP.setupPrimitiveContainer}}>
88
<button
99
type="button"
1010
class="hds-breadcrumb__truncation-toggle"
1111
aria-label={{this.ariaLabel}}
12-
aria-expanded={{if t.isOpen "true" "false"}}
13-
{{on "click" t.onClickToggle}}
12+
aria-expanded={{if PP.isOpen "true" "false"}}
13+
{{PP.setupPrimitiveToggle}}
1414
>
1515
<FlightIcon @name="more-horizontal" @size="16" @isInlineBlock={{false}} />
1616
</button>
17-
</:toggle>
18-
<:content>
19-
<div class="hds-breadcrumb__truncation-content">
17+
<div
18+
class="hds-breadcrumb__truncation-content"
19+
{{PP.setupPrimitivePopover anchoredPositionOptions=(hash placement="bottom-start" offsetOptions=4)}}
20+
>
2021
<ol class="hds-breadcrumb__sublist">
2122
{{yield}}
2223
</ol>
2324
</div>
24-
</:content>
25-
</Hds::MenuPrimitive>
25+
</div>
26+
</Hds::PopoverPrimitive>
2627
</li>

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

+15-11
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,26 @@
22
Copyright (c) HashiCorp, Inc.
33
SPDX-License-Identifier: MPL-2.0
44
}}
5-
<Hds::MenuPrimitive class={{this.classNames}} @onClose={{@onClose}} ...attributes>
6-
<:toggle as |t|>
5+
<Hds::PopoverPrimitive @isOpen={{@isOpen}} @onClose={{@onClose}} @enableClickEvents={{true}} as |PP|>
6+
<div class={{this.classNames}} ...attributes {{PP.setupPrimitiveContainer}}>
77
{{yield
88
(hash
9-
ToggleButton=(component "hds/dropdown/toggle/button" isOpen=t.isOpen onClick=t.onClickToggle)
10-
ToggleIcon=(component "hds/dropdown/toggle/icon" isOpen=t.isOpen onClick=t.onClickToggle)
9+
ToggleButton=(component
10+
"hds/dropdown/toggle/button" isOpen=PP.isOpen setupPrimitiveToggle=PP.setupPrimitiveToggle
11+
)
12+
ToggleIcon=(component "hds/dropdown/toggle/icon" isOpen=PP.isOpen setupPrimitiveToggle=PP.setupPrimitiveToggle)
1113
)
1214
}}
13-
</:toggle>
14-
<:content as |c|>
15-
<div class={{this.classNamesContent}} {{style width=@width max-height=@height}}>
15+
<div
16+
class={{this.classNamesContent}}
17+
{{style width=@width max-height=@height}}
18+
{{PP.setupPrimitivePopover anchoredPositionOptions=this.anchoredPositionOptions}}
19+
>
1620
{{yield (hash Header=(component "hds/dropdown/header"))}}
1721
<ul class="hds-dropdown__list" {{did-insert this.didInsertList}}>
1822
{{yield
1923
(hash
20-
close=c.close
24+
close=PP.hidePopover
2125
Checkbox=(component "hds/dropdown/list-item/checkbox")
2226
Checkmark=(component "hds/dropdown/list-item/checkmark")
2327
CopyItem=(component "hds/dropdown/list-item/copy-item")
@@ -30,7 +34,7 @@
3034
)
3135
}}
3236
</ul>
33-
{{yield (hash close=c.close Footer=(component "hds/dropdown/footer"))}}
37+
{{yield (hash close=PP.hidePopover Footer=(component "hds/dropdown/footer"))}}
3438
</div>
35-
</:content>
36-
</Hds::MenuPrimitive>
39+
</div>
40+
</Hds::PopoverPrimitive>

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

+30-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ import Component from '@glimmer/component';
77
import { action } from '@ember/object';
88
import { assert } from '@ember/debug';
99

10-
import { HdsDropdownPositionValues } from './types.ts';
10+
import {
11+
// map Dropdown's `listPosition` values to PopoverPrimitive's `placement` values
12+
HdsDropdownPositionToPlacementValues,
13+
// Dropdown's `listPosition` values
14+
HdsDropdownPositionValues,
15+
} from './types.ts';
1116

1217
import type { ComponentLike } from '@glint/template';
1318
import type { MenuPrimitiveSignature } from '../menu-primitive';
@@ -26,15 +31,19 @@ import type { HdsDropdownToggleButtonSignature } from './toggle/button';
2631
import type { HdsDropdownToggleIconSignature } from './toggle/icon';
2732
import type { HdsDropdownPositions } from './types';
2833

34+
import type { FloatingUIOptions } from '../../../modifiers/hds-anchored-position.ts';
35+
2936
export const DEFAULT_POSITION = HdsDropdownPositionValues.BottomRight;
3037
export const POSITIONS: string[] = Object.values(HdsDropdownPositionValues);
3138

3239
export interface HdsDropdownSignature {
3340
Args: MenuPrimitiveSignature['Args'] & {
3441
height?: string;
3542
isInline?: boolean;
43+
isOpen?: boolean;
3644
listPosition?: HdsDropdownPositions;
3745
width?: string;
46+
enableCollisionDetection?: FloatingUIOptions['enableCollisionDetection'];
3847
};
3948
Blocks: {
4049
default: [
@@ -79,6 +88,24 @@ export default class HdsDropdownComponent extends Component<HdsDropdownSignature
7988
return listPosition;
8089
}
8190

91+
get enableCollisionDetection(): FloatingUIOptions['enableCollisionDetection'] {
92+
return this.args.enableCollisionDetection ?? false;
93+
}
94+
95+
get anchoredPositionOptions(): {
96+
placement: FloatingUIOptions['placement'];
97+
offsetOptions: FloatingUIOptions['offsetOptions'];
98+
enableCollisionDetection: FloatingUIOptions['enableCollisionDetection'];
99+
} {
100+
// custom options specific for the `RichTooltip` component
101+
// for details see the `hds-anchored-position` modifier
102+
return {
103+
placement: HdsDropdownPositionToPlacementValues[this.listPosition],
104+
offsetOptions: 4,
105+
enableCollisionDetection: this.enableCollisionDetection ? 'flip' : false,
106+
};
107+
}
108+
82109
/**
83110
* Get the class names to apply to the element
84111
* @method classNames
@@ -104,6 +131,8 @@ export default class HdsDropdownComponent extends Component<HdsDropdownSignature
104131
const classes = ['hds-dropdown__content'];
105132

106133
// add a class based on the @listPosition argument
134+
// TODO: we preserved these classes to avoid introducing breaking changes for consumers who rely on these classes for tests, but we aim to remove them in the next major release
135+
// context: https://github.com/hashicorp/design-system/pull/2309#discussion_r1706941892
107136
classes.push(`hds-dropdown__content--position-${this.listPosition}`);
108137

109138
// add a class based on the @width argument

packages/components/src/components/hds/dropdown/toggle/button.hbs

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
...attributes
99
type="button"
1010
aria-expanded={{if @isOpen "true" "false"}}
11-
{{on "click" this.onClick}}
11+
{{@setupPrimitiveToggle}}
1212
>
1313
{{#if @icon}}
1414
<div class="hds-dropdown-toggle-button__icon">

packages/components/src/components/hds/dropdown/toggle/button.ts

+3-20
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import type {
1818
HdsDropdownToggleButtonSizes,
1919
HdsDropdownToggleButtonColors,
2020
} from './types';
21+
import type { ModifierLike } from '@glint/template';
22+
import type { SetupPrimitiveToggleModifier } from '../../popover-primitive/index.ts';
2123

2224
export const DEFAULT_SIZE = HdsDropdownToggleButtonSizeValues.Medium;
2325
export const DEFAULT_COLOR = HdsDropdownToggleButtonColorValues.Primary;
@@ -26,8 +28,6 @@ export const COLORS: string[] = Object.values(
2628
HdsDropdownToggleButtonColorValues
2729
);
2830

29-
const NOOP = (): void => {};
30-
3131
export interface HdsDropdownToggleButtonSignature {
3232
Args: {
3333
badge?: HdsBadgeSignature['Args']['text'];
@@ -37,9 +37,9 @@ export interface HdsDropdownToggleButtonSignature {
3737
icon?: FlightIconSignature['Args']['name'];
3838
isFullWidth?: boolean;
3939
isOpen?: boolean;
40-
onClick?: (event: MouseEvent) => void;
4140
size: HdsDropdownToggleButtonSizes;
4241
text: string;
42+
setupPrimitiveToggle?: ModifierLike<SetupPrimitiveToggleModifier>;
4343
};
4444
Element: HTMLButtonElement;
4545
}
@@ -116,23 +116,6 @@ export default class HdsDropdownToggleButtonComponent extends Component<HdsDropd
116116
return this.args.isFullWidth ?? false;
117117
}
118118

119-
/**
120-
* @param onClick
121-
* @type {function}
122-
* @default () => {}
123-
*/
124-
get onClick(): (event: MouseEvent) => void {
125-
const { onClick } = this.args;
126-
127-
// notice: this is a guard used in case the toggle is used as standalone element (eg. in the showcase)
128-
// in reality it's always used inside the Dropdown main component as yielded component, so the onClick handler is always defined
129-
if (typeof onClick === 'function') {
130-
return onClick;
131-
} else {
132-
return NOOP;
133-
}
134-
}
135-
136119
/**
137120
* @param badgeType
138121
* @type {string}

packages/components/src/components/hds/dropdown/toggle/icon.hbs

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
aria-label={{this.text}}
88
...attributes
99
aria-expanded={{if @isOpen "true" "false"}}
10-
{{on "click" this.onClick}}
10+
{{@setupPrimitiveToggle}}
1111
{{did-update this.onDidUpdateImageSrc @imageSrc}}
1212
type="button"
1313
>

packages/components/src/components/hds/dropdown/toggle/icon.ts

+3-20
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,21 @@ import { HdsDropdownToggleIconSizeValues } from './types.ts';
1111

1212
import type { FlightIconSignature } from '@hashicorp/ember-flight-icons/components/flight-icon';
1313
import type { HdsDropdownToggleIconSizes } from './types';
14+
import type { ModifierLike } from '@glint/template';
15+
import type { SetupPrimitiveToggleModifier } from '../../popover-primitive/index.ts';
1416

1517
export const DEFAULT_SIZE = HdsDropdownToggleIconSizeValues.Medium;
1618
export const SIZES: string[] = Object.values(HdsDropdownToggleIconSizeValues);
1719

18-
const NOOP = (): void => {};
19-
2020
export interface HdsDropdownToggleIconSignature {
2121
Args: {
2222
hasChevron?: boolean;
2323
icon: FlightIconSignature['Args']['name'];
2424
imageSrc: string;
2525
isOpen?: boolean;
26-
onClick?: (event: MouseEvent) => void;
2726
size?: HdsDropdownToggleIconSizes;
2827
text: string;
28+
setupPrimitiveToggle?: ModifierLike<SetupPrimitiveToggleModifier>;
2929
};
3030
Element: HTMLButtonElement;
3131
}
@@ -114,23 +114,6 @@ export default class HdsDropdownToggleIconComponent extends Component<HdsDropdow
114114
return this.args.hasChevron ?? true;
115115
}
116116

117-
/**
118-
* @param onClick
119-
* @type {function}
120-
* @default () => {}
121-
*/
122-
get onClick(): (event: MouseEvent) => void {
123-
const { onClick } = this.args;
124-
125-
// notice: this is a guard used in case the toggle is used as standalone element (eg. in the showcase)
126-
// in reality it's always used inside the Dropdown main component as yielded component, so the onClick handler is always defined
127-
if (typeof onClick === 'function') {
128-
return onClick;
129-
} else {
130-
return NOOP;
131-
}
132-
}
133-
134117
/**
135118
* Get the class names to apply to the component.
136119
* @method ToggleIcon#classNames

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

+14
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,24 @@
33
* SPDX-License-Identifier: MPL-2.0
44
*/
55

6+
import type { FloatingUIOptions } from '../../../modifiers/hds-anchored-position.ts';
7+
68
export enum HdsDropdownPositionValues {
79
BottomLeft = 'bottom-left',
810
BottomRight = 'bottom-right',
911
TopLeft = 'top-left',
1012
TopRight = 'top-right',
1113
}
1214
export type HdsDropdownPositions = `${HdsDropdownPositionValues}`;
15+
16+
// map Dropdown's `listPosition` values to PopoverPrimitive's `placement` values for backwards compatibility
17+
export const HdsDropdownPositionToPlacementValues: Record<
18+
// Dropdown's `listPosition` values
19+
HdsDropdownPositionValues,
20+
FloatingUIOptions['placement']
21+
> = {
22+
[HdsDropdownPositionValues.BottomLeft]: 'bottom-start',
23+
[HdsDropdownPositionValues.BottomRight]: 'bottom-end',
24+
[HdsDropdownPositionValues.TopLeft]: 'top-start',
25+
[HdsDropdownPositionValues.TopRight]: 'top-end',
26+
};

packages/components/src/components/hds/menu-primitive/index.hbs

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
Copyright (c) HashiCorp, Inc.
33
SPDX-License-Identifier: MPL-2.0
44
}}
5+
{{!
6+
THIS COMPONENT IS NOW DEPRECATED
7+
}}
58
{{! template-lint-disable no-invalid-interactive }}
69
<div
710
class="hds-menu-primitive"

packages/components/src/components/hds/menu-primitive/index.ts

+19
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import Component from '@glimmer/component';
7+
import { deprecate } from '@ember/debug';
78
import { tracked } from '@glimmer/tracking';
89
import { action } from '@ember/object';
910
import { schedule } from '@ember/runloop';
@@ -36,6 +37,24 @@ export default class MenuPrimitiveComponent extends Component<MenuPrimitiveSigna
3637
@tracked toggleRef: HTMLElement | undefined;
3738
@tracked element!: HTMLElement;
3839

40+
constructor(owner: unknown, args: MenuPrimitiveSignature['Args']) {
41+
super(owner, args);
42+
43+
deprecate(
44+
'The `Hds::MenuPrimitive` component is now deprecated and will be removed in the next major version of `@hashicorp/design-system-components`.',
45+
false,
46+
{
47+
id: 'hds.components.menu-primitive',
48+
until: '5.0.0',
49+
url: 'https://helios.hashicorp.design/components/menu-primitive?tab=version%20history#460',
50+
for: '@hashicorp/design-system-components',
51+
since: {
52+
enabled: '4.10.0',
53+
},
54+
}
55+
);
56+
}
57+
3958
@action
4059
didInsert(element: HTMLElement): void {
4160
this.element = element;

packages/components/src/styles/components/breadcrumb.scss

+14-5
Original file line numberDiff line numberDiff line change
@@ -181,15 +181,24 @@ $hds-breadcrumb-item-visual-horizontal-padding: 4px;
181181
}
182182

183183
.hds-breadcrumb__truncation-content {
184-
position: absolute;
185-
top: 100%;
186-
left: -$hds-breadcrumb-item-visual-horizontal-padding;
187-
z-index: 300; // this is the z-index used in Structure for this kind of things, I am reusing the same value
184+
position: relative;
188185
width: max-content;
189186
max-width: 200px; // by design
190-
margin-top: 4px;
191187
padding: 6px 12px;
192188
background-color: var(--token-color-surface-primary);
193189
border-radius: 6px;
194190
box-shadow: var(--token-surface-high-box-shadow);
191+
192+
// the "popover" attributes comes with pre-defined styling so we need to override it
193+
:where(&[popover]) {
194+
width: fit-content;
195+
height: fit-content;
196+
margin: 0;
197+
padding: 0;
198+
overflow: visible;
199+
color: inherit;
200+
background: none;
201+
border: none;
202+
inset: 0;
203+
}
195204
}

0 commit comments

Comments
 (0)