@descope-ui/descope-attachment 3.2.0
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/CHANGELOG.md +10 -0
- package/e2e/attachment.spec.ts +29 -0
- package/package.json +32 -0
- package/project.json +8 -0
- package/src/component/AttachmentClass.js +152 -0
- package/src/component/index.js +5 -0
- package/src/theme.js +52 -0
- package/stories/descope-attachment.stories.js +113 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
|
|
4
|
+
|
|
5
|
+
## [3.2.0](https://github.com/descope/web-components-ui/compare/web-components-ui-3.1.13...web-components-ui-3.2.0) (2026-04-16)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* add descope-last-auth-badge component ([#982](https://github.com/descope/web-components-ui/issues/982)) ([055b461](https://github.com/descope/web-components-ui/commit/055b4618abbdf518494a2984e56d66e58fdb4809))
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test';
|
|
2
|
+
import { getStoryUrl, loopConfig } from 'e2e-utils';
|
|
3
|
+
|
|
4
|
+
const componentAttributes = {
|
|
5
|
+
position: [
|
|
6
|
+
'top-start',
|
|
7
|
+
'top-end',
|
|
8
|
+
'top-center',
|
|
9
|
+
'bottom-start',
|
|
10
|
+
'bottom-end',
|
|
11
|
+
'bottom-center',
|
|
12
|
+
],
|
|
13
|
+
'offset-x': [0, 20, -20],
|
|
14
|
+
'offset-y': [0, 20, -20],
|
|
15
|
+
direction: ['ltr', 'rtl'],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const storyName = 'descope-attachment';
|
|
19
|
+
|
|
20
|
+
test.describe('theme', () => {
|
|
21
|
+
loopConfig(componentAttributes, (attr, value) => {
|
|
22
|
+
test(`${attr}: ${value}`, async ({ page }) => {
|
|
23
|
+
await page.goto(getStoryUrl(storyName, { [attr]: value }), {
|
|
24
|
+
waitUntil: 'networkidle',
|
|
25
|
+
});
|
|
26
|
+
expect(await page.screenshot()).toMatchSnapshot();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@descope-ui/descope-attachment",
|
|
3
|
+
"version": "3.2.0",
|
|
4
|
+
"exports": {
|
|
5
|
+
".": {
|
|
6
|
+
"import": "./src/component/index.js"
|
|
7
|
+
},
|
|
8
|
+
"./theme": {
|
|
9
|
+
"import": "./src/theme.js"
|
|
10
|
+
},
|
|
11
|
+
"./class": {
|
|
12
|
+
"import": "./src/component/AttachmentClass.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@playwright/test": "1.58.2",
|
|
17
|
+
"e2e-utils": "3.2.0",
|
|
18
|
+
"test-assets": "3.2.0",
|
|
19
|
+
"test-drivers": "3.2.0"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@descope-ui/common": "3.2.0",
|
|
23
|
+
"@descope-ui/theme-globals": "3.2.0"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"link-workspace-packages": false
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "echo 'No tests defined' && exit 0",
|
|
30
|
+
"test:e2e": "playwright test"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/project.json
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import {
|
|
2
|
+
componentNameValidationMixin,
|
|
3
|
+
createStyleMixin,
|
|
4
|
+
draggableMixin,
|
|
5
|
+
} from '@descope-ui/common/components-mixins';
|
|
6
|
+
import { createBaseClass } from '@descope-ui/common/base-classes';
|
|
7
|
+
import {
|
|
8
|
+
getComponentName,
|
|
9
|
+
injectStyle,
|
|
10
|
+
} from '@descope-ui/common/components-helpers';
|
|
11
|
+
import { compose } from '@descope-ui/common/utils';
|
|
12
|
+
|
|
13
|
+
export const componentName = getComponentName('attachment');
|
|
14
|
+
|
|
15
|
+
const ATTACHMENT_POSITIONS = [
|
|
16
|
+
'top-end',
|
|
17
|
+
'top-start',
|
|
18
|
+
'top-center',
|
|
19
|
+
'bottom-start',
|
|
20
|
+
'bottom-end',
|
|
21
|
+
'bottom-center',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const DEFAULT_POSITION = ATTACHMENT_POSITIONS[0];
|
|
25
|
+
|
|
26
|
+
class RawAttachment extends createBaseClass({
|
|
27
|
+
componentName,
|
|
28
|
+
baseSelector: ':host > .wrapper',
|
|
29
|
+
}) {
|
|
30
|
+
constructor() {
|
|
31
|
+
super();
|
|
32
|
+
|
|
33
|
+
this.attachShadow({ mode: 'open' }).innerHTML = `
|
|
34
|
+
<div class="wrapper">
|
|
35
|
+
<slot></slot>
|
|
36
|
+
<div class="attachment-container">
|
|
37
|
+
<slot name="attachment"></slot>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
this.defaultSlot = this.shadowRoot.querySelector('slot:not([name])');
|
|
43
|
+
this.attachmentSlot = this.shadowRoot.querySelector(
|
|
44
|
+
'slot[name="attachment"]',
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static get observedAttributes() {
|
|
49
|
+
return [...(super.observedAttributes || []), 'position'];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
attributeChangedCallback(name, oldVal, newVal) {
|
|
53
|
+
super.attributeChangedCallback?.(name, oldVal, newVal);
|
|
54
|
+
if (name === 'position' && oldVal !== newVal) {
|
|
55
|
+
this.#handlePositionChange();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
init() {
|
|
60
|
+
super.init?.();
|
|
61
|
+
|
|
62
|
+
injectStyle(
|
|
63
|
+
`
|
|
64
|
+
:host {
|
|
65
|
+
display: inline-block;
|
|
66
|
+
}
|
|
67
|
+
.wrapper {
|
|
68
|
+
position: relative;
|
|
69
|
+
display: inline-flex;
|
|
70
|
+
}
|
|
71
|
+
.attachment-container {
|
|
72
|
+
position: absolute;
|
|
73
|
+
z-index: 1;
|
|
74
|
+
pointer-events: none;
|
|
75
|
+
width: 100%;
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
container-type: inline-size;
|
|
79
|
+
}
|
|
80
|
+
:host(.hidden) {
|
|
81
|
+
display: none;
|
|
82
|
+
}
|
|
83
|
+
`,
|
|
84
|
+
this,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
this.#handlePositionChange();
|
|
88
|
+
|
|
89
|
+
this.defaultSlot.addEventListener('slotchange', () => {
|
|
90
|
+
this.#setVisibility();
|
|
91
|
+
this.#syncDirection();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
window.requestAnimationFrame(() => {
|
|
95
|
+
this.#setVisibility();
|
|
96
|
+
this.#syncDirection();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#setVisibility() {
|
|
101
|
+
const hasAnchor = this.defaultSlot?.assignedElements()?.length > 0;
|
|
102
|
+
this.classList.toggle('hidden', !hasAnchor);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#syncDirection() {
|
|
106
|
+
const child = this.defaultSlot?.assignedElements()?.[0];
|
|
107
|
+
if (!child) return;
|
|
108
|
+
|
|
109
|
+
const { direction } = window.getComputedStyle(child);
|
|
110
|
+
|
|
111
|
+
// currently we support direction sync only for web-components-ui
|
|
112
|
+
// elements, which support st-host-direction attribute.
|
|
113
|
+
this.attachmentSlot?.assignedElements().forEach((el) => {
|
|
114
|
+
el.setAttribute('st-host-direction', direction);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
get offsetX() {
|
|
119
|
+
return this.getAttribute('offset-x') || '0px';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
get offsetY() {
|
|
123
|
+
return this.getAttribute('offset-y') || '0px';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#handlePositionChange() {
|
|
127
|
+
const pos = this.getAttribute('position');
|
|
128
|
+
if (!ATTACHMENT_POSITIONS.includes(pos)) {
|
|
129
|
+
this.setAttribute('position', DEFAULT_POSITION);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const attachmentContainer = { selector: () => '.attachment-container' };
|
|
135
|
+
|
|
136
|
+
export const AttachmentClass = compose(
|
|
137
|
+
createStyleMixin({
|
|
138
|
+
mappings: {
|
|
139
|
+
transform: { ...attachmentContainer },
|
|
140
|
+
justifyContent: { ...attachmentContainer },
|
|
141
|
+
maxWidth: { ...attachmentContainer },
|
|
142
|
+
top: { ...attachmentContainer },
|
|
143
|
+
bottom: { ...attachmentContainer },
|
|
144
|
+
left: { ...attachmentContainer },
|
|
145
|
+
right: { ...attachmentContainer },
|
|
146
|
+
offsetX: {},
|
|
147
|
+
offsetY: {},
|
|
148
|
+
},
|
|
149
|
+
}),
|
|
150
|
+
draggableMixin,
|
|
151
|
+
componentNameValidationMixin,
|
|
152
|
+
)(RawAttachment);
|
package/src/theme.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useVar } from '@descope-ui/common/theme-helpers';
|
|
2
|
+
import { AttachmentClass } from './component/AttachmentClass';
|
|
3
|
+
|
|
4
|
+
const vars = AttachmentClass.cssVarList;
|
|
5
|
+
|
|
6
|
+
const defaultOffset = '8px';
|
|
7
|
+
const insetCalc = `calc(${defaultOffset} + var(${vars.offsetX}))`;
|
|
8
|
+
|
|
9
|
+
const attachment = {
|
|
10
|
+
[vars.maxWidth]: `calc(100% - 2 * ${defaultOffset})`,
|
|
11
|
+
|
|
12
|
+
position: {
|
|
13
|
+
'top-start': {
|
|
14
|
+
[vars.transform]: 'translateY(-50%)',
|
|
15
|
+
[vars.justifyContent]: 'start',
|
|
16
|
+
[vars.left]: insetCalc,
|
|
17
|
+
[vars.top]: useVar(vars.offsetY),
|
|
18
|
+
},
|
|
19
|
+
'top-center': {
|
|
20
|
+
[vars.transform]: 'translate(-50%, -50%)',
|
|
21
|
+
[vars.justifyContent]: 'center',
|
|
22
|
+
[vars.top]: useVar(vars.offsetY),
|
|
23
|
+
[vars.left]: '50%',
|
|
24
|
+
},
|
|
25
|
+
'top-end': {
|
|
26
|
+
[vars.transform]: 'translateY(-50%)',
|
|
27
|
+
[vars.justifyContent]: 'end',
|
|
28
|
+
[vars.right]: insetCalc,
|
|
29
|
+
[vars.top]: useVar(vars.offsetY),
|
|
30
|
+
},
|
|
31
|
+
'bottom-start': {
|
|
32
|
+
[vars.transform]: 'translateY(50%)',
|
|
33
|
+
[vars.justifyContent]: 'start',
|
|
34
|
+
[vars.left]: insetCalc,
|
|
35
|
+
[vars.bottom]: useVar(vars.offsetY),
|
|
36
|
+
},
|
|
37
|
+
'bottom-center': {
|
|
38
|
+
[vars.bottom]: useVar(vars.offsetY),
|
|
39
|
+
[vars.transform]: 'translate(-50%, 50%)',
|
|
40
|
+
[vars.justifyContent]: 'center',
|
|
41
|
+
[vars.left]: '50%',
|
|
42
|
+
},
|
|
43
|
+
'bottom-end': {
|
|
44
|
+
[vars.transform]: 'translateY(50%)',
|
|
45
|
+
[vars.justifyContent]: 'end',
|
|
46
|
+
[vars.right]: insetCalc,
|
|
47
|
+
[vars.bottom]: useVar(vars.offsetY),
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default attachment;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { directionControl } from '@descope-ui/common/sb-controls';
|
|
2
|
+
import { componentName } from '../src/component';
|
|
3
|
+
import { base64svg } from 'test-assets';
|
|
4
|
+
|
|
5
|
+
const SIZES = ['xs', 'sm', 'md', 'lg'];
|
|
6
|
+
|
|
7
|
+
const button = (size, { direction, label } = {}) => `
|
|
8
|
+
<descope-button
|
|
9
|
+
variant="outline"
|
|
10
|
+
mode="primary"
|
|
11
|
+
size="${size}"
|
|
12
|
+
${direction ? ` st-host-direction="${direction}"` : ''}
|
|
13
|
+
${label ? '' : ' square="true"'}>
|
|
14
|
+
${label || `<descope-icon src=${base64svg}></descope-icon>`}
|
|
15
|
+
</descope-button>
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
const attachment = (position, anchor, badgeLabel, offsetX, offsetY) => `
|
|
19
|
+
<descope-attachment position="${position}" st-offset-x="${offsetX}px" st-offset-y="${offsetY}px">
|
|
20
|
+
${anchor}
|
|
21
|
+
<descope-badge
|
|
22
|
+
slot="attachment"
|
|
23
|
+
mode="primary"
|
|
24
|
+
variant="contained"
|
|
25
|
+
size="xs"
|
|
26
|
+
shadow="sm"
|
|
27
|
+
bordered="true"
|
|
28
|
+
shrink-to-indicator="true"
|
|
29
|
+
st-offset-x="${offsetX}px"
|
|
30
|
+
st-offset-y="${offsetY}px"
|
|
31
|
+
>
|
|
32
|
+
${badgeLabel}
|
|
33
|
+
</descope-badge>
|
|
34
|
+
</descope-attachment>`;
|
|
35
|
+
|
|
36
|
+
const Template = ({
|
|
37
|
+
position,
|
|
38
|
+
buttonLabel,
|
|
39
|
+
badgeLabel,
|
|
40
|
+
direction,
|
|
41
|
+
'offset-x': offsetX,
|
|
42
|
+
'offset-y': offsetY,
|
|
43
|
+
}) => `
|
|
44
|
+
<div class="item">
|
|
45
|
+
${SIZES.map((size) =>
|
|
46
|
+
attachment(
|
|
47
|
+
position,
|
|
48
|
+
button(size, { direction, label: buttonLabel }),
|
|
49
|
+
badgeLabel,
|
|
50
|
+
offsetX,
|
|
51
|
+
offsetY,
|
|
52
|
+
),
|
|
53
|
+
).join('')}
|
|
54
|
+
${SIZES.map((size) =>
|
|
55
|
+
attachment(position, button(size), badgeLabel, offsetX, offsetY),
|
|
56
|
+
).join('')}
|
|
57
|
+
</div>
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
export default {
|
|
61
|
+
component: componentName,
|
|
62
|
+
title: 'descope-attachment',
|
|
63
|
+
parameters: {
|
|
64
|
+
controls: { expanded: true },
|
|
65
|
+
},
|
|
66
|
+
decorators: [
|
|
67
|
+
(story) => `
|
|
68
|
+
<style nonce="${window.DESCOPE_NONCE}">
|
|
69
|
+
.item {
|
|
70
|
+
display: flex;
|
|
71
|
+
flex-direction: column;
|
|
72
|
+
align-items: flex-start;
|
|
73
|
+
gap: 16px;
|
|
74
|
+
}
|
|
75
|
+
</style>
|
|
76
|
+
${story()}
|
|
77
|
+
`,
|
|
78
|
+
],
|
|
79
|
+
argTypes: {
|
|
80
|
+
position: {
|
|
81
|
+
control: { type: 'select' },
|
|
82
|
+
options: [
|
|
83
|
+
'top-end',
|
|
84
|
+
'top-start',
|
|
85
|
+
'top-center',
|
|
86
|
+
'bottom-end',
|
|
87
|
+
'bottom-start',
|
|
88
|
+
'bottom-center',
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
...directionControl,
|
|
92
|
+
badgeLabel: {
|
|
93
|
+
name: 'Badge Label',
|
|
94
|
+
control: 'text',
|
|
95
|
+
},
|
|
96
|
+
'offset-x': {
|
|
97
|
+
control: 'number',
|
|
98
|
+
},
|
|
99
|
+
'offset-y': {
|
|
100
|
+
control: 'number',
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const Default = Template.bind({});
|
|
106
|
+
Default.args = {
|
|
107
|
+
position: 'top-end',
|
|
108
|
+
badgeLabel: 'Last used -',
|
|
109
|
+
buttonLabel: 'Sign in with Google -',
|
|
110
|
+
direction: 'ltr',
|
|
111
|
+
'offset-x': 0,
|
|
112
|
+
'offset-y': 2,
|
|
113
|
+
};
|