Skip to content

Commit 0c7dfc9

Browse files
authored
Refactor accordion styling to use contextual tokens (#2570)
1 parent 6c72794 commit 0c7dfc9

File tree

9 files changed

+275
-122
lines changed

9 files changed

+275
-122
lines changed

.changeset/grumpy-paws-jam.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@sl-design-system/accordion': minor
3+
---
4+
5+
Various improvements:
6+
- Add a global and per instance `iconType` property with default value `plusminus` and new option `chevron`
7+
- Add `::part(details)` so you can remove the bottom border from the last `<sl-accordion-item>`
8+
- Add `summary-extras` slot for extra content in the header of the accordion item
9+
- Reduce `<summary>` font-size from 18px to 16px
10+
- Use relative font-size and inherit the font-family
11+
- Refactor styling to use contextual tokens

packages/components/accordion/src/accordion-item.scss

Lines changed: 67 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,5 @@
11
:host {
2-
--_background: var(--sl-color-accordion-default-header);
3-
--_background-hover: var(--sl-color-accordion-hover-header);
4-
--_background-active: var(--sl-color-accordion-active-header);
5-
--_border-color: var(--sl-color-accordion-default-border);
6-
--_border-width: var(--sl-border-width-divider);
7-
--_focus-outline: var(--sl-color-focusring-default) solid var(--sl-border-width-focusring-default);
8-
--_focus-outline-offset: var(--sl-border-width-focusring-offset);
9-
--_focus-radius: var(--sl-border-radius-focusring-default);
10-
--_icon-size: var(--sl-size-accordion-icon-height);
11-
--_padding-block: var(--sl-space-accordion-content-block);
12-
--_panel-color: var(--sl-color-accordion-default-foreground);
13-
--_panel-font: var(--sl-text-accordion-body);
14-
--_panel-padding-block: var(--sl-space-accordion-content-block);
15-
--_panel-padding-inline: var(--sl-space-accordion-content-inline);
16-
--_title-color: var(--sl-color-accordion-default-foreground);
17-
--_title-color-active: var(--sl-color-accordion-active-foreground);
18-
--_title-color-disabled: var(--sl-color-accordion-disabled-foreground);
19-
--_title-color-hover: var(--sl-color-accordion-hover-foreground);
20-
--_title-font: var(--sl-text-accordion-title);
21-
--_title-gap: var(--sl-space-accordion-title-gap);
22-
--_title-padding: var(--sl-space-accordion-title-inline);
23-
--_transition-duration: 300ms;
2+
--_transition-duration: 0.3s; // This is used in JavaScript as well
243

254
display: flex;
265
outline: 0;
@@ -30,22 +9,35 @@
309
pointer-events: none;
3110

3211
summary {
33-
--_title-color: var(--_title-color-disabled);
12+
color: var(--sl-color-foreground-disabled);
13+
}
14+
}
15+
16+
:host([icon-type='chevron']) {
17+
details[open],
18+
details.opening {
19+
sl-icon {
20+
transform: rotate(-180deg);
21+
}
22+
}
23+
24+
details[open].closing sl-icon {
25+
transform: rotate(0deg);
3426
}
3527
}
3628

3729
details {
38-
border-block-end: var(--_border-width) solid var(--_border-color);
30+
border-block-end: var(--sl-size-borderWidth-subtle) solid var(--sl-color-border-plain);
3931
flex: 1;
4032

4133
&[open] summary::after {
42-
background: var(--_border-color);
43-
block-size: var(--_border-width);
34+
background: var(--sl-color-border-plain);
35+
block-size: var(--sl-size-borderWidth-subtle);
4436
content: '';
45-
inline-size: calc(100% - 2 * var(--_panel-padding-inline));
37+
inline-size: calc(100% - 2 * var(--sl-size-500));
4638
inset-block-end: 0;
4739
inset-inline-start: 0;
48-
margin-inline: var(--_panel-padding-inline);
40+
margin-inline: var(--sl-size-500);
4941
position: absolute;
5042
}
5143

@@ -81,14 +73,23 @@ details {
8173
}
8274

8375
summary {
84-
background: var(--_background);
85-
color: var(--_title-color);
76+
--_bg-opacity: var(--sl-opacity-interactive-bold-idle);
77+
78+
// align-items: center;
79+
background: color-mix(
80+
in srgb,
81+
var(--sl-elevation-surface-base-default),
82+
var(--sl-color-background-secondary-interactive-plain) calc(100% * var(--_bg-opacity))
83+
);
84+
color: var(--sl-color-foreground-plain);
8685
cursor: pointer;
8786
display: flex;
88-
font: var(--_title-font);
89-
gap: var(--_title-gap);
90-
padding-block: var(--_padding-block);
91-
padding-inline: var(--_title-padding);
87+
font-size: calc((16 / 14) * 1em);
88+
gap: var(--sl-size-100);
89+
line-height: calc((24 / 16) * 1em);
90+
outline: transparent solid var(--sl-size-borderWidth-focusRing);
91+
outline-offset: var(--sl-size-outlineOffset-default);
92+
padding: var(--sl-size-200);
9293
position: relative;
9394
z-index: 1; /* To work properly with sticky */
9495

@@ -97,31 +98,50 @@ summary {
9798
}
9899

99100
&:hover {
100-
background: var(--_background-hover);
101-
color: var(--_title-color-hover);
101+
--_bg-opacity: var(--sl-opacity-interactive-bold-hover);
102102
}
103103

104104
&:active {
105-
background: var(--_background-active);
106-
color: var(--_title-color-active);
105+
--_bg-opacity: var(--sl-opacity-interactive-bold-active);
107106
}
108107

109108
&:focus-visible {
110-
border-radius: var(--_focus-radius);
111-
outline: var(--_focus-outline);
112-
outline-offset: var(--_focus-outline-offset);
109+
border-radius: var(--sl-size-borderRadius-default);
110+
outline-color: var(--sl-color-border-focused);
113111
position: relative;
114112
z-index: 2; // Make sure the outline is not clipped by the next accordion item
115113
}
116114

117115
@media (prefers-reduced-motion: no-preference) {
118-
transition: background var(--_transition-duration);
116+
transition-duration: var(--_transition-duration);
117+
transition-property: background, outline-color;
119118
}
120119
}
121120

122-
svg {
123-
block-size: var(--_icon-size);
121+
slot[name='summary'] {
122+
display: block;
123+
inline-size: 100%;
124+
text-box: trim-both cap alphabetic;
125+
}
126+
127+
svg,
128+
sl-icon {
129+
align-self: start;
124130
flex-shrink: 0;
131+
margin-block-start: round(up, 1ex - 1cap, 1px);
132+
}
133+
134+
sl-icon {
135+
block-size: var(--sl-size-200);
136+
137+
@media (prefers-reduced-motion: no-preference) {
138+
transition: transform var(--_transition-duration) ease-in-out;
139+
}
140+
}
141+
142+
svg {
143+
aspect-ratio: 1;
144+
block-size: var(--sl-size-200);
125145
}
126146

127147
g {
@@ -131,7 +151,6 @@ g {
131151
transform: rotate(90deg);
132152
}
133153

134-
/* stylelint-disable-next-line max-nesting-depth */
135154
@media (prefers-reduced-motion: no-preference) {
136155
transition: transform var(--_transition-duration) cubic-bezier(0.6, 2, 0.6, 1);
137156
}
@@ -142,7 +161,7 @@ g {
142161
animation-fill-mode: both;
143162
animation-iteration-count: 1;
144163
animation-timing-function: linear;
145-
background: var(--_background);
164+
background: var(--sl-elevation-surface-base-default);
146165
display: grid;
147166
overflow: hidden;
148167

@@ -157,9 +176,8 @@ g {
157176
}
158177

159178
[part='panel'] {
160-
color: var(--_panel-color);
161-
font: var(--_panel-font);
162-
padding: var(--_panel-padding-block) var(--_panel-padding-inline);
179+
color: var(--sl-color-foreground-plain);
180+
padding: var(--sl-size-200) var(--sl-size-500);
163181
position: relative;
164182
}
165183

packages/components/accordion/src/accordion-item.spec.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,40 @@ describe('sl-accordion-item', () => {
1919
summary = el.renderRoot.querySelector('summary') as HTMLElement;
2020
});
2121

22-
it('should render correctly', () => {
23-
expect(el).shadowDom.to.equalSnapshot();
24-
});
25-
2622
it('should not be disabled', () => {
2723
expect(el).not.to.have.attribute('disabled');
2824
expect(el.disabled).not.to.be.true;
2925
});
3026

27+
it('should not have an icon type', () => {
28+
expect(el).not.to.have.attribute('icon-type');
29+
expect(el.iconType).to.be.undefined;
30+
});
31+
32+
it('should have an icon type when set', async () => {
33+
el.iconType = 'chevron';
34+
await el.updateComplete;
35+
36+
expect(el).to.have.attribute('icon-type', 'chevron');
37+
});
38+
39+
it('should render a custom svg icon', () => {
40+
const icon = el.renderRoot.querySelector('svg');
41+
42+
expect(icon).to.exist;
43+
expect(icon).to.contain('g.horizontal-line');
44+
expect(icon).to.contain('g.vertical-line');
45+
});
46+
47+
it('should render an sl-icon when icon type is "chevron"', async () => {
48+
el.iconType = 'chevron';
49+
await el.updateComplete;
50+
51+
const icon = el.renderRoot.querySelector('sl-icon');
52+
expect(icon).to.exist;
53+
expect(icon).to.have.attribute('name', 'chevron-down');
54+
});
55+
3156
it('should have the correct attributes', () => {
3257
expect(summary).to.have.attribute('aria-controls', 'content');
3358
expect(summary).to.have.attribute('aria-expanded', 'false');

packages/components/accordion/src/accordion-item.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { type SlToggleEvent } from '@sl-design-system/shared/events.js';
44
import { type CSSResultGroup, LitElement, type PropertyValues, type TemplateResult, html } from 'lit';
55
import { property } from 'lit/decorators.js';
66
import styles from './accordion-item.scss.js';
7+
import { type AccordionIconType } from './accordion.js';
78

89
declare global {
910
interface HTMLElementTagNameMap {
@@ -14,11 +15,13 @@ declare global {
1415
/**
1516
* An accordion item component.
1617
*
18+
* @csspart details - Details element of the accordion-item
1719
* @csspart summary - Header element of the accordion-item
1820
* @csspart panel - The body of the accordion-item
1921
*
2022
* @slot default - Body content for the accordion
2123
* @slot summary - Header content for the accordion; use this if the `summary` property is not enough
24+
* @slot summary-extras - Extra content in the header of the accordion item
2225
*/
2326
@localized()
2427
export class AccordionItem extends LitElement {
@@ -28,6 +31,9 @@ export class AccordionItem extends LitElement {
2831
/** Whether the element is disabled. */
2932
@property({ type: Boolean, reflect: true }) disabled?: boolean;
3033

34+
/** @internal */
35+
@property({ attribute: 'icon-type', reflect: true }) iconType?: AccordionIconType;
36+
3137
/** Whether the details element is opened. */
3238
@property({ type: Boolean, reflect: true }) open?: boolean;
3339

@@ -55,7 +61,7 @@ export class AccordionItem extends LitElement {
5561

5662
override render(): TemplateResult {
5763
return html`
58-
<details @toggle=${this.#onToggle}>
64+
<details @toggle=${this.#onToggle} part="details">
5965
<summary
6066
@click=${this.#onClick}
6167
aria-controls="content"
@@ -64,15 +70,20 @@ export class AccordionItem extends LitElement {
6470
part="summary"
6571
tabindex=${this.disabled ? -1 : 0}
6672
>
67-
<svg viewBox="-12 -14 24 28" xmlns="http://www.w3.org/2000/svg">
68-
<g class="horizontal-line">
69-
<rect x="-1" y="-8" width="2" height="16" rx="0.824742" fill="currentColor" />
70-
</g>
71-
<g class="vertical-line">
72-
<rect x="-1" y="-8" width="2" height="16" rx="0.824742" fill="currentColor" />
73-
</g>
74-
</svg>
73+
${this.iconType === 'chevron'
74+
? html`<sl-icon name="chevron-down"></sl-icon>`
75+
: html`
76+
<svg viewBox="-8 -8 16 16" xmlns="http://www.w3.org/2000/svg">
77+
<g class="horizontal-line">
78+
<rect x="-1" y="-7" width="2" height="14" rx="0.82" fill="currentColor" />
79+
</g>
80+
<g class="vertical-line">
81+
<rect x="-1" y="-7" width="2" height="14" rx="0.82" fill="currentColor" />
82+
</g>
83+
</svg>
84+
`}
7585
<slot name="summary">${this.summary}</slot>
86+
<slot name="summary-extras"></slot>
7687
</summary>
7788
<div class="wrapper">
7889
<div class="body">
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
:host {
2-
--_background: var(--sl-body-background);
3-
4-
background: var(--_background);
2+
background: var(--sl-elevation-surface-base-default);
53
display: block;
64
}

packages/components/accordion/src/accordion.spec.ts

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,6 @@ import { Accordion } from './accordion.js';
77
describe('sl-accordion', () => {
88
let el: Accordion;
99

10-
describe('empty', () => {
11-
beforeEach(async () => {
12-
el = await fixture(html`<sl-accordion></sl-accordion>`);
13-
});
14-
15-
it('should not break', () => {
16-
expect(el).shadowDom.to.equalSnapshot();
17-
});
18-
});
19-
2010
describe('defaults', () => {
2111
beforeEach(async () => {
2212
el = await fixture(html`
@@ -28,8 +18,27 @@ describe('sl-accordion', () => {
2818
`);
2919
});
3020

31-
it('should render correctly', () => {
32-
expect(el).shadowDom.to.equalSnapshot();
21+
it('should have icon type "plusminus"', () => {
22+
expect(el.iconType).to.equal('plusminus');
23+
});
24+
25+
it('should propagate the icon type to all items', () => {
26+
const iconTypes = Array.from(el.querySelectorAll('sl-accordion-item')).map(item =>
27+
item.getAttribute('icon-type')
28+
);
29+
30+
expect(iconTypes).to.deep.equal(['plusminus', 'plusminus', 'plusminus']);
31+
});
32+
33+
it('should have icon type "chevron" when set', async () => {
34+
el.iconType = 'chevron';
35+
await el.updateComplete;
36+
37+
const iconTypes = Array.from(el.querySelectorAll('sl-accordion-item')).map(item =>
38+
item.getAttribute('icon-type')
39+
);
40+
41+
expect(iconTypes).to.deep.equal(['chevron', 'chevron', 'chevron']);
3342
});
3443

3544
it('should not be in single mode', () => {
@@ -100,4 +109,21 @@ describe('sl-accordion', () => {
100109
expect(items.at(1)?.open).to.be.true;
101110
});
102111
});
112+
113+
describe('global icon type', () => {
114+
it('should have a default icon type of "plusminus"', () => {
115+
expect(Accordion.iconType).to.equal('plusminus');
116+
});
117+
118+
it('should allow setting a global icon type', async () => {
119+
Accordion.iconType = 'chevron';
120+
121+
el = await fixture(html`<sl-accordion></sl-accordion>`);
122+
123+
expect(el.iconType).to.equal('chevron');
124+
125+
// Reset for future tests
126+
Accordion.iconType = 'plusminus';
127+
});
128+
});
103129
});

0 commit comments

Comments
 (0)