@m3e/slide-group 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +116 -0
- package/cem.config.mjs +16 -0
- package/dist/css-custom-data.json +27 -0
- package/dist/custom-elements.json +260 -0
- package/dist/html-custom-data.json +38 -0
- package/dist/index.js +355 -0
- package/dist/index.js.map +1 -0
- package/dist/index.min.js +120 -0
- package/dist/index.min.js.map +1 -0
- package/dist/src/SlideGroupElement.d.ts +90 -0
- package/dist/src/SlideGroupElement.d.ts.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -0
- package/eslint.config.mjs +13 -0
- package/package.json +49 -0
- package/rollup.config.js +32 -0
- package/src/SlideGroupElement.ts +253 -0
- package/src/index.ts +1 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { css, CSSResultGroup, html, LitElement, nothing, PropertyValues } from "lit";
|
|
2
|
+
import { customElement, property, query, state } from "lit/decorators.js";
|
|
3
|
+
|
|
4
|
+
import { debounce, ResizeController, Role } from "@m3e/core";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @summary
|
|
8
|
+
* Presents pagination controls used to scroll overflowing content.
|
|
9
|
+
*
|
|
10
|
+
* @description
|
|
11
|
+
* The `m3e-slide-group` component presents directional pagination controls for navigating overflowing content.
|
|
12
|
+
* It orchestrates scrollable layouts with expressive slot-based icons and adaptive orientation, revealing navigation
|
|
13
|
+
* affordances only when content exceeds a defined threshold. It supports both horizontal and vertical flows, and
|
|
14
|
+
* encodes accessibility through customizable labels and interaction states.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* The following example illustrates a horizontally scrollable group of items with directional pagination buttons.
|
|
18
|
+
* The scroll controls appear when content exceeds the `48px` threshold.
|
|
19
|
+
* ```html
|
|
20
|
+
* <m3e-slide-group threshold="48">
|
|
21
|
+
* <div>Item 1</div>
|
|
22
|
+
* <div>Item 2</div>
|
|
23
|
+
* <div>Item 3</div>
|
|
24
|
+
* <div>Item 4</div>
|
|
25
|
+
* <div>Item 5</div>
|
|
26
|
+
* </m3e-slide-group>
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @tag m3e-slide-group
|
|
30
|
+
*
|
|
31
|
+
* @slot - Renders the content to paginate.
|
|
32
|
+
* @slot next-icon - Renders the icon to present for the next button.
|
|
33
|
+
* @slot prev-icon - Renders the icon to present for the previous button.
|
|
34
|
+
*
|
|
35
|
+
* @attr disabled - Whether scroll buttons are disabled.
|
|
36
|
+
* @attr next-page-label - The accessible label given to the button used to move to the previous page.
|
|
37
|
+
* @attr previous-page-label - The accessible label given to the button used to move to the next page.
|
|
38
|
+
* @attr threshold - A value, in pixels, indicating the scroll threshold at which to begin showing pagination controls.
|
|
39
|
+
* @attr vertical - Whether content is oriented vertically.
|
|
40
|
+
*
|
|
41
|
+
* @cssprop --m3e-slide-group-button-icon-size - Sets icon size for scroll buttons; overrides default small icon size.
|
|
42
|
+
* @cssprop --m3e-slide-group-button-size - Defines scroll button size; used for width (horizontal) or height (vertical).
|
|
43
|
+
* @cssprop --m3e-slide-group-divider-top - Adds top border to content container for visual separation.
|
|
44
|
+
* @cssprop --m3e-slide-group-divider-bottom - Adds bottom border to content container for visual separation.
|
|
45
|
+
*/
|
|
46
|
+
@customElement("m3e-slide-group")
|
|
47
|
+
export class M3eSlideGroupElement extends Role(LitElement, "none") {
|
|
48
|
+
/** The styles of the element. */
|
|
49
|
+
static override styles: CSSResultGroup = css`
|
|
50
|
+
:host {
|
|
51
|
+
display: flex;
|
|
52
|
+
flex-wrap: nowrap;
|
|
53
|
+
overflow: hidden;
|
|
54
|
+
}
|
|
55
|
+
:host([vertical]) {
|
|
56
|
+
flex-direction: column;
|
|
57
|
+
}
|
|
58
|
+
.prev-button,
|
|
59
|
+
.next-button {
|
|
60
|
+
flex: none;
|
|
61
|
+
--m3e-icon-button-small-shape-round: 0px;
|
|
62
|
+
--m3e-icon-button-small-shape-square: 0px;
|
|
63
|
+
--m3e-icon-button-small-shape-pressed-morph: 0px;
|
|
64
|
+
--m3e-focus-ring-visibility: hidden;
|
|
65
|
+
}
|
|
66
|
+
::slotted(prev-icon),
|
|
67
|
+
::slotted(next-icon),
|
|
68
|
+
.icon {
|
|
69
|
+
width: 1em;
|
|
70
|
+
font-size: var(--m3e-slide-group-button-icon-size, var(--m3e-icon-button-small-icon-size, 1.5rem)) !important;
|
|
71
|
+
}
|
|
72
|
+
:host(:not([vertical])) .prev-button,
|
|
73
|
+
:host(:not([vertical])) .next-button {
|
|
74
|
+
--m3e-icon-button-small-container-height: 100%;
|
|
75
|
+
width: var(--m3e-slide-group-button-size, 2.5rem);
|
|
76
|
+
}
|
|
77
|
+
:host([vertical]) .prev-button,
|
|
78
|
+
:host([vertical]) .next-button {
|
|
79
|
+
width: unset;
|
|
80
|
+
--m3e-icon-button-small-container-height: var(--m3e-slide-group-button-size, 2.5rem);
|
|
81
|
+
}
|
|
82
|
+
:host([vertical]) .prev-button .icon {
|
|
83
|
+
transform: rotate(-90deg);
|
|
84
|
+
}
|
|
85
|
+
:host([vertical]) .next-button .icon {
|
|
86
|
+
transform: rotate(90deg);
|
|
87
|
+
}
|
|
88
|
+
.content {
|
|
89
|
+
flex: 1 1 auto;
|
|
90
|
+
display: inherit;
|
|
91
|
+
flex-wrap: inherit;
|
|
92
|
+
flex-direction: inherit;
|
|
93
|
+
overflow: inherit;
|
|
94
|
+
position: relative;
|
|
95
|
+
border-top: var(--m3e-slide-group-divider-top);
|
|
96
|
+
border-bottom: var(--m3e-slide-group-divider-bottom);
|
|
97
|
+
}
|
|
98
|
+
`;
|
|
99
|
+
|
|
100
|
+
/** @private */
|
|
101
|
+
readonly #resizeController = new ResizeController(this, {
|
|
102
|
+
target: null,
|
|
103
|
+
callback: () => this._updatePaging(),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/** @internal A reference to the container used to scroll content. */
|
|
107
|
+
@query(".content") scrollContainer!: HTMLElement;
|
|
108
|
+
|
|
109
|
+
/** @private */ @state() private _canPage = false;
|
|
110
|
+
/** @private */ @state() private _canPageStart = false;
|
|
111
|
+
/** @private */ @state() private _canPageEnd = false;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Whether scroll buttons are disabled.
|
|
115
|
+
* @default false
|
|
116
|
+
*/
|
|
117
|
+
@property({ type: Boolean, reflect: true }) disabled = false;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Whether content is oriented vertically.
|
|
121
|
+
* @default false
|
|
122
|
+
*/
|
|
123
|
+
@property({ type: Boolean, reflect: true }) vertical = false;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A value, in pixels, indicating the scroll threshold at which to begin showing pagination controls.
|
|
127
|
+
* @default 0
|
|
128
|
+
*/
|
|
129
|
+
@property({ type: Number }) threshold = 0;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The accessible label given to the button used to move to the previous page.
|
|
133
|
+
* @default "Previous page"
|
|
134
|
+
*/
|
|
135
|
+
@property({ attribute: "previous-page-label" }) previousPageLabel = "Previous page";
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The accessible label given to the button used to move to the next page.
|
|
139
|
+
* @default "Next page"
|
|
140
|
+
*/
|
|
141
|
+
@property({ attribute: "next-page-label" }) nextPageLabel = "Next page";
|
|
142
|
+
|
|
143
|
+
/** @inheritdoc */
|
|
144
|
+
protected override firstUpdated(_changedProperties: PropertyValues): void {
|
|
145
|
+
super.firstUpdated(_changedProperties);
|
|
146
|
+
this.#resizeController.observe(this.scrollContainer);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** @inheritdoc */
|
|
150
|
+
protected override render(): unknown {
|
|
151
|
+
const prevButton = html`<m3e-icon-button
|
|
152
|
+
class="prev-button"
|
|
153
|
+
tabindex="-1"
|
|
154
|
+
aria-label="${this.previousPageLabel}"
|
|
155
|
+
?disabled="${!this._canPageStart}"
|
|
156
|
+
@click="${this.#pageStart}"
|
|
157
|
+
>
|
|
158
|
+
<slot name="prev-icon">
|
|
159
|
+
<svg class="icon" viewBox="0 -960 960 960" fill="currentColor">
|
|
160
|
+
<path d="M640-80 240-480l400-400 71 71-329 329 329 329-71 71Z" />
|
|
161
|
+
</svg>
|
|
162
|
+
</slot>
|
|
163
|
+
</m3e-icon-button>`;
|
|
164
|
+
|
|
165
|
+
const nextButton = html`<m3e-icon-button
|
|
166
|
+
class="next-button"
|
|
167
|
+
tabindex="-1"
|
|
168
|
+
aria-label="${this.nextPageLabel}"
|
|
169
|
+
?disabled="${!this._canPageEnd}"
|
|
170
|
+
@click="${this.#pageEnd}"
|
|
171
|
+
>
|
|
172
|
+
<slot name="next-icon">
|
|
173
|
+
<svg class="icon" viewBox="0 -960 960 960" fill="currentColor">
|
|
174
|
+
<path d="m321-80-71-71 329-329-329-329 71-71 400 400L321-80Z" />
|
|
175
|
+
</svg>
|
|
176
|
+
</slot>
|
|
177
|
+
</m3e-icon-button>`;
|
|
178
|
+
|
|
179
|
+
return html`${this._canPage ? prevButton : nothing}
|
|
180
|
+
<div class="content" @scroll="${this._updatePaging}"><slot></slot></div>
|
|
181
|
+
${this._canPage ? nextButton : nothing}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** @private */
|
|
185
|
+
#pageStart(): void {
|
|
186
|
+
if (!this.vertical) {
|
|
187
|
+
let left = this.scrollContainer.scrollLeft - this.scrollContainer.clientWidth;
|
|
188
|
+
if (left <= this.threshold) {
|
|
189
|
+
left = 0;
|
|
190
|
+
}
|
|
191
|
+
this.scrollContainer.scrollTo({ left, behavior: "smooth" });
|
|
192
|
+
} else {
|
|
193
|
+
let top = this.scrollContainer.scrollTop - this.scrollContainer.clientHeight;
|
|
194
|
+
if (top <= this.threshold) {
|
|
195
|
+
top = 0;
|
|
196
|
+
}
|
|
197
|
+
this.scrollContainer.scrollTo({ top, behavior: "smooth" });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** @private */
|
|
202
|
+
#pageEnd(): void {
|
|
203
|
+
if (!this.vertical) {
|
|
204
|
+
let left = this.scrollContainer.scrollLeft + this.scrollContainer.clientWidth;
|
|
205
|
+
if (left >= this.scrollContainer.scrollWidth - this.scrollContainer.clientWidth - this.threshold) {
|
|
206
|
+
left = this.scrollContainer.scrollWidth - this.scrollContainer.clientWidth;
|
|
207
|
+
}
|
|
208
|
+
this.scrollContainer.scrollTo({ left, behavior: "smooth" });
|
|
209
|
+
} else {
|
|
210
|
+
let top = this.scrollContainer.scrollTop + this.scrollContainer.clientHeight;
|
|
211
|
+
if (top >= this.scrollContainer.scrollHeight - this.scrollContainer.clientHeight - this.threshold) {
|
|
212
|
+
top = this.scrollContainer.scrollHeight - this.scrollContainer.clientHeight;
|
|
213
|
+
}
|
|
214
|
+
this.scrollContainer.scrollTo({ top, behavior: "smooth" });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** @private */
|
|
219
|
+
@debounce(40)
|
|
220
|
+
private _updatePaging(): void {
|
|
221
|
+
if (this.disabled) {
|
|
222
|
+
this._canPage = false;
|
|
223
|
+
} else if (!this.vertical) {
|
|
224
|
+
this._canPage =
|
|
225
|
+
Math.round(this.scrollContainer.scrollWidth) > Math.round(this.scrollContainer.clientWidth) + this.threshold;
|
|
226
|
+
if (this._canPage) {
|
|
227
|
+
this._canPageStart = Math.round(this.scrollContainer.scrollLeft) > this.threshold;
|
|
228
|
+
this._canPageEnd =
|
|
229
|
+
Math.round(this.scrollContainer.scrollLeft) + this.threshold <
|
|
230
|
+
Math.round(this.scrollContainer.scrollWidth - this.scrollContainer.clientWidth);
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
this._canPage =
|
|
234
|
+
Math.round(this.scrollContainer.scrollHeight) > Math.round(this.scrollContainer.clientHeight) + this.threshold;
|
|
235
|
+
if (this._canPage) {
|
|
236
|
+
this._canPageStart = Math.round(this.scrollContainer.scrollTop) > this.threshold;
|
|
237
|
+
this._canPageEnd =
|
|
238
|
+
Math.round(this.scrollContainer.scrollTop) + +this.threshold <
|
|
239
|
+
Math.round(this.scrollContainer.scrollHeight - this.scrollContainer.clientHeight);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!this._canPage) {
|
|
244
|
+
this._canPageStart = this._canPageEnd = false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
declare global {
|
|
250
|
+
interface HTMLElementTagNameMap {
|
|
251
|
+
"m3e-slide-group": M3eSlideGroupElement;
|
|
252
|
+
}
|
|
253
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./SlideGroupElement";
|