Skip to content

Commit 83163a8

Browse files
committed
feat: add style api to dropdown
1 parent 6189c3d commit 83163a8

File tree

16 files changed

+464
-232
lines changed

16 files changed

+464
-232
lines changed

build-tools/utils/custom-css-properties.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ const customCssPropertiesList = [
5151
// Dropdown Custom Properties
5252
'dropdownDefaultMaxWidth',
5353
'dropdownDefaultMinWidth',
54+
// Dropdown Style API
55+
'dropdownContentBorderColor',
56+
'dropdownContentBorderWidth',
57+
'dropdownContentBorderRadius',
5458
// Modal Custom Properties
5559
'modalCustomWidth',
5660
'modalCustomHeight',
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import React, { useState } from 'react';
4+
5+
import Button from '~components/button';
6+
import Dropdown from '~components/dropdown/internal';
7+
import SpaceBetween from '~components/space-between';
8+
9+
import { palette } from '../app/themes/style-api';
10+
import ScreenshotArea from '../utils/screenshot-area';
11+
import ListContent from './list-content';
12+
13+
export default function DropdownStylePermutations() {
14+
const [open1, setOpen1] = useState(false);
15+
const [open2, setOpen2] = useState(false);
16+
17+
return (
18+
<ScreenshotArea>
19+
<h1>Dropdown Style Permutations</h1>
20+
<SpaceBetween direction="vertical" size="xl">
21+
<section>
22+
<h2>Custom background, no border</h2>
23+
<Dropdown
24+
trigger={<Button onClick={() => setOpen1(!open1)}>Open dropdown</Button>}
25+
open={open1}
26+
onOutsideClick={() => setOpen1(false)}
27+
content={<ListContent n={5} />}
28+
style={{
29+
dropdown: {
30+
background: `light-dark(${palette.blue10}, ${palette.blue90})`,
31+
borderWidth: '0px',
32+
},
33+
}}
34+
/>
35+
</section>
36+
37+
<section>
38+
<h2>Custom background with styled border</h2>
39+
<Dropdown
40+
trigger={<Button onClick={() => setOpen2(!open2)}>Open dropdown</Button>}
41+
open={open2}
42+
onOutsideClick={() => setOpen2(false)}
43+
content={<ListContent n={5} />}
44+
style={{
45+
dropdown: {
46+
background: `light-dark(${palette.teal10}, ${palette.teal90})`,
47+
borderColor: `light-dark(${palette.teal60}, ${palette.teal40})`,
48+
borderRadius: '12px',
49+
borderWidth: '2px',
50+
},
51+
}}
52+
/>
53+
</section>
54+
</SpaceBetween>
55+
</ScreenshotArea>
56+
);
57+
}

src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11720,6 +11720,52 @@ because fixed positioning results in a slight, visible lag when scrolling comple
1172011720
"optional": true,
1172111721
"type": "boolean",
1172211722
},
11723+
{
11724+
"description": "An object containing CSS properties to customize the dropdown's visual appearance.",
11725+
"inlineType": {
11726+
"name": "DropdownProps.Style",
11727+
"properties": [
11728+
{
11729+
"inlineType": {
11730+
"name": "object",
11731+
"properties": [
11732+
{
11733+
"name": "background",
11734+
"optional": true,
11735+
"type": "string",
11736+
},
11737+
{
11738+
"name": "borderColor",
11739+
"optional": true,
11740+
"type": "string",
11741+
},
11742+
{
11743+
"name": "borderRadius",
11744+
"optional": true,
11745+
"type": "string",
11746+
},
11747+
{
11748+
"name": "borderWidth",
11749+
"optional": true,
11750+
"type": "string",
11751+
},
11752+
],
11753+
"type": "object",
11754+
},
11755+
"name": "dropdown",
11756+
"optional": true,
11757+
"type": "{ background?: string | undefined; borderColor?: string | undefined; borderRadius?: string | undefined; borderWidth?: string | undefined; }",
11758+
},
11759+
],
11760+
"type": "object",
11761+
},
11762+
"name": "style",
11763+
"optional": true,
11764+
"systemTags": [
11765+
"core",
11766+
],
11767+
"type": "DropdownProps.Style",
11768+
},
1172311769
],
1172411770
"regions": [
1172511771
{
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Dropdown Component Style API all style properties 1`] = `
4+
CSSStyleDeclaration {
5+
"0": "background",
6+
"1": "--awsui-dropdown-content-border-color-6190tg",
7+
"2": "--awsui-dropdown-content-border-width-6190tg",
8+
"3": "--awsui-dropdown-content-border-radius-6190tg",
9+
"_importants": {
10+
"--awsui-dropdown-content-border-color-6190tg": undefined,
11+
"--awsui-dropdown-content-border-radius-6190tg": undefined,
12+
"--awsui-dropdown-content-border-width-6190tg": undefined,
13+
"background": undefined,
14+
},
15+
"_length": 4,
16+
"_onChange": [Function],
17+
"_values": {
18+
"--awsui-dropdown-content-border-color-6190tg": "rgb(0, 0, 0)",
19+
"--awsui-dropdown-content-border-radius-6190tg": "8px",
20+
"--awsui-dropdown-content-border-width-6190tg": "2px",
21+
"background": "rgb(255, 255, 255)",
22+
"background-color": "rgb(255, 255, 255)",
23+
},
24+
}
25+
`;
26+
27+
exports[`Dropdown Component Style API no style applied when style prop is not set 1`] = `
28+
CSSStyleDeclaration {
29+
"_importants": {},
30+
"_length": 0,
31+
"_onChange": [Function],
32+
"_values": {},
33+
}
34+
`;

src/dropdown/__tests__/dropdown.test.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,36 @@ describe('Dropdown Component', () => {
416416
});
417417
});
418418

419+
describe('Style API', () => {
420+
test('all style properties', () => {
421+
const [wrapper] = renderDropdown(
422+
<Dropdown
423+
trigger={<button />}
424+
open={true}
425+
content={<div>content</div>}
426+
style={{
427+
dropdown: {
428+
background: 'rgb(255, 255, 255)',
429+
borderColor: 'rgb(0, 0, 0)',
430+
borderRadius: '8px',
431+
borderWidth: '2px',
432+
},
433+
}}
434+
/>
435+
);
436+
const openDropdown = wrapper.findOpenDropdown()!.getElement();
437+
const wrapperEl = openDropdown.querySelector<HTMLElement>('[class*="dropdown-content-wrapper"]')!;
438+
expect(wrapperEl.style).toMatchSnapshot();
439+
});
440+
441+
test('no style applied when style prop is not set', () => {
442+
const [wrapper] = renderDropdown(<Dropdown trigger={<button />} open={true} content={<div>content</div>} />);
443+
const openDropdown = wrapper.findOpenDropdown()!.getElement();
444+
const wrapperEl = openDropdown.querySelector<HTMLElement>('[class*="dropdown-content-wrapper"]')!;
445+
expect(wrapperEl.style).toMatchSnapshot();
446+
});
447+
});
448+
419449
describe('width CSS variables', () => {
420450
test('applies numeric minWidth value as pixels', () => {
421451
const [wrapper] = renderDropdown(<Dropdown trigger={<button />} open={true} minWidth={300} />);

src/dropdown/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ export { DropdownProps };
1313
/**
1414
* @awsuiSystem core
1515
*/
16-
export default function Dropdown({ ...props }: DropdownProps) {
16+
export default function Dropdown({ style, ...props }: DropdownProps) {
1717
const baseComponentProps = useBaseComponent('Dropdown');
18-
return <InternalDropdown {...props} {...baseComponentProps} />;
18+
return <InternalDropdown {...props} style={style} {...baseComponentProps} />;
1919
}
2020
applyDisplayName(Dropdown, 'Dropdown');

src/dropdown/interfaces.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,29 @@ import { NonCancelableEventHandler } from '../internal/events';
66

77
export type OptionsFilteringType = 'none' | 'auto' | 'manual';
88

9+
export namespace DropdownProps {
10+
export interface Style {
11+
dropdown?: {
12+
/**
13+
* Background color of the dropdown content wrapper.
14+
*/
15+
background?: string;
16+
/**
17+
* Border color of the dropdown content wrapper.
18+
*/
19+
borderColor?: string;
20+
/**
21+
* Border radius of the dropdown content wrapper.
22+
*/
23+
borderRadius?: string;
24+
/**
25+
* Border width of the dropdown content wrapper.
26+
*/
27+
borderWidth?: string;
28+
};
29+
}
30+
}
31+
932
/**
1033
* Alignment of the dropdown relative to its trigger.
1134
*/
@@ -144,6 +167,12 @@ export interface DropdownProps extends ExpandToViewport {
144167
* Adds `aria-describedby` to the dropdown content element.
145168
*/
146169
ariaDescribedby?: string;
170+
171+
/**
172+
* An object containing CSS properties to customize the dropdown's visual appearance.
173+
* @awsuiSystem core
174+
*/
175+
style?: DropdownProps.Style;
147176
}
148177

149178
export interface ExpandToViewport {

src/dropdown/internal.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
} from './dropdown-fit-handler';
2929
import { applyDropdownPositionRelativeToViewport, LogicalDOMRect } from './dropdown-position';
3030
import { DropdownAlignment, DropdownProps, DropdownWidthConstraint } from './interfaces';
31+
import { getDropdownStyles } from './style';
3132

3233
export interface InternalDropdownProps
3334
extends Omit<DropdownProps, 'minWidth' | 'maxWidth'>,
@@ -110,6 +111,7 @@ interface TransitionContentProps {
110111
ariaLabel?: string;
111112
ariaLabelledby?: string;
112113
ariaDescribedby?: string;
114+
dropdownStyle?: React.CSSProperties;
113115
}
114116

115117
const TransitionContent = ({
@@ -138,6 +140,7 @@ const TransitionContent = ({
138140
ariaLabel,
139141
ariaLabelledby,
140142
ariaDescribedby,
143+
dropdownStyle,
141144
}: TransitionContentProps) => {
142145
const contentRef = useMergeRefs(dropdownRef, transitionRef);
143146
const dropdownStyles: Record<string, string> = {};
@@ -178,6 +181,7 @@ const TransitionContent = ({
178181
!header && !content && styles['is-empty'],
179182
isRefresh && styles.refresh
180183
)}
184+
style={dropdownStyle}
181185
>
182186
<div ref={verticalContainerRef} className={styles['dropdown-content']}>
183187
<DropdownContextProvider position={position}>
@@ -221,6 +225,7 @@ const InternalDropdown = ({
221225
ariaLabel,
222226
ariaLabelledby,
223227
ariaDescribedby,
228+
style,
224229
__internalRootRef,
225230
...restProps
226231
}: InternalDropdownProps) => {
@@ -580,6 +585,7 @@ const InternalDropdown = ({
580585
ariaLabel={ariaLabel}
581586
ariaLabelledby={ariaLabelledby}
582587
ariaDescribedby={ariaDescribedby}
588+
dropdownStyle={getDropdownStyles(style)}
583589
/>
584590

585591
<TabTrap

src/dropdown/style.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import { SYSTEM } from '../internal/environment';
4+
import customCssProps from '../internal/generated/custom-css-properties';
5+
import { DropdownProps } from './interfaces';
6+
7+
export function getDropdownStyles(style: DropdownProps['style']) {
8+
if (SYSTEM !== 'core' || !style?.dropdown) {
9+
return undefined;
10+
}
11+
12+
return {
13+
background: style.dropdown.background,
14+
[customCssProps.dropdownContentBorderColor]: style.dropdown.borderColor,
15+
[customCssProps.dropdownContentBorderWidth]: style.dropdown.borderWidth,
16+
[customCssProps.dropdownContentBorderRadius]: style.dropdown.borderRadius,
17+
};
18+
}

src/dropdown/styles.scss

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@
4545
position: relative;
4646
background-color: awsui.$color-background-dropdown-item-default;
4747
outline: none;
48-
@include styles.dropdown-shadow;
48+
@include styles.dropdown-shadow(
49+
$border-color: var(#{custom-props.$dropdownContentBorderColor}, #{awsui.$color-border-dropdown-container}),
50+
$border-width: var(#{custom-props.$dropdownContentBorderWidth}, #{awsui.$border-width-popover}),
51+
$border-radius: var(#{custom-props.$dropdownContentBorderRadius}, #{awsui.$border-radius-dropdown})
52+
);
4953

5054
// Don't show dropdown border when there's no dropdown content
5155
&.is-empty::after {

0 commit comments

Comments
 (0)