@eturnity/eturnity_reusable_components 9.37.2 → 9.37.3-ATE-87.2

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/README.md CHANGED
@@ -10,7 +10,8 @@ npm install @eturnity/eturnity_reusable_components
10
10
 
11
11
  ### Setting up the theme
12
12
 
13
- The library now includes its own theme. You need to wrap your application with the `ThemeProvider` component from this library.
13
+ The library now includes its own theme. You need to wrap your application with
14
+ the `ThemeProvider` component from this library.
14
15
 
15
16
  **Recommended approach - Using the exported theme:**
16
17
 
@@ -66,7 +67,8 @@ import InputText from '@eturnity/eturnity_reusable_components/src/components/inp
66
67
 
67
68
  ### Overriding the theme (optional)
68
69
 
69
- If you need to customize or override the theme, you can pass your own theme when installing the plugin:
70
+ If you need to customize or override the theme, you can pass your own theme when
71
+ installing the plugin:
70
72
 
71
73
  ```javascript
72
74
  import EturnityComponentsPlugin, {
@@ -124,22 +126,32 @@ Customers load provided files from jsDelivr using stable tag-based URLs:
124
126
  <script src="https://cdn.jsdelivr.net/npm/@eturnity/eturnity_reusable_components@latest-consent-bridge/dist/eturnity-consent-bridge.min.js"></script>
125
127
  ```
126
128
 
127
- Consent bridge integration documentation is maintained in `docs/eturnity-consent-bridge.md` and published to:
129
+ Consent bridge integration documentation is maintained in
130
+ `docs/eturnity-consent-bridge.md` and published to:
128
131
 
129
132
  ```text
130
133
  https://cdn.jsdelivr.net/npm/@eturnity/eturnity_reusable_components@latest-consent-bridge/dist/eturnity-consent-bridge.md
131
134
  ```
132
135
 
133
- Provided files and docs are configured in `provided-assets.config.js`. Script and doc entries can both define their own `versionTag` (for example `latest-consent-bridge`). Because customers keep these URLs unchanged, we update what they receive by moving those npm dist-tags to a published package version.
136
+ Provided files and docs are configured in `provided-assets.config.js`. Script
137
+ and doc entries can both define their own `versionTag` (for example
138
+ `latest-consent-bridge`). Because customers keep these URLs unchanged, we update
139
+ what they receive by moving those npm dist-tags to a published package version.
134
140
 
135
141
  ### Build and publish behavior
136
142
 
137
- - `npm run build` runs `vite build` (library), `npm run build:provided-files` (JS CDN bundles), and `npm run copy:provided-docs` (published docs).
138
- - `npm run build:provided-files` builds files listed in `provided-assets.config.js` into `dist/*.min.js`.
139
- - `npm run copy:provided-docs` copies docs listed in `provided-assets.config.js` into `dist/*.md`.
140
- - `npm run verify:provided-files` checks expected provided files exist in `dist/` and are non-empty.
141
- - `npm run verify:provided-docs` checks expected provided docs exist in `dist/` and are non-empty.
142
- - `prepack` and `prepublishOnly` run both verification steps to reduce broken publish risk.
143
+ - `npm run build` runs `vite build` (library), `npm run build:provided-files`
144
+ (JS CDN bundles), and `npm run copy:provided-docs` (published docs).
145
+ - `npm run build:provided-files` builds files listed in
146
+ `provided-assets.config.js` into `dist/*.min.js`.
147
+ - `npm run copy:provided-docs` copies docs listed in `provided-assets.config.js`
148
+ into `dist/*.md`.
149
+ - `npm run verify:provided-files` checks expected provided files exist in
150
+ `dist/` and are non-empty.
151
+ - `npm run verify:provided-docs` checks expected provided docs exist in `dist/`
152
+ and are non-empty.
153
+ - `prepack` and `prepublishOnly` run both verification steps to reduce broken
154
+ publish risk.
143
155
 
144
156
  ### Standard release steps
145
157
 
@@ -149,9 +161,11 @@ Provided files and docs are configured in `provided-assets.config.js`. Script an
149
161
  npm publish
150
162
  ```
151
163
 
152
- 2. Optional: promote provided-asset tag(s) so customer script/doc URLs point to that version.
164
+ 2. Optional: promote provided-asset tag(s) so customer script/doc URLs point to
165
+ that version.
153
166
 
154
- Use this only if you want to update what customers receive from stable CDN tag URLs:
167
+ Use this only if you want to update what customers receive from stable CDN tag
168
+ URLs:
155
169
 
156
170
  Script options (`--dry-run`, `--version=…`, `--name=…`, and combinations)
157
171
 
@@ -196,7 +210,8 @@ Optional tuning via environment variables:
196
210
 
197
211
  ### Optional rollback (re-point customer URL to older stable package)
198
212
 
199
- If a promoted version is broken, you can optionally promote a previously published version:
213
+ If a promoted version is broken, you can optionally promote a previously
214
+ published version:
200
215
 
201
216
  ```bash
202
217
  npm run promote:provided-files-tag -- --version=<older-published-version>
@@ -217,11 +232,19 @@ npm run promote:provided-files-tag -- --version=9.15.2
217
232
 
218
233
  Unit tests are Jest + `@vue/test-utils`, co-located with each component as
219
234
  `componentName.spec.js`. Run a single spec with `pnpm test <path>` or the full
220
- suite with `pnpm test`. See [`docs/component-testing.md`](docs/component-testing.md)
221
- for how we write tests (conventions, the TDD workflow, and using Claude Code).
235
+ suite with `pnpm test`. See
236
+ [`docs/component-testing.md`](docs/component-testing.md) for how we write tests
237
+ (conventions, the TDD workflow, and using Claude Code).
222
238
 
223
239
  ## CI and pull requests
224
240
 
225
- Opening or updating a pull request runs **Bitbucket Pipelines** ([`bitbucket-pipelines.yml`](bitbucket-pipelines.yml)): install with pnpm, **`pnpm test`**, then **`pnpm run build`**. PRs are expected to pass this pipeline.
241
+ Opening or updating a pull request runs **Bitbucket Pipelines**
242
+ ([`bitbucket-pipelines.yml`](bitbucket-pipelines.yml)): install with pnpm,
243
+ **`pnpm test`**, then **`pnpm run build`**. PRs are expected to pass this
244
+ pipeline.
226
245
 
227
- **Blocking merges on failure** is configured in **Bitbucket repository settings** (not in this repo): enable merge checks that require a successful pull-request build—for example a minimum number of successful builds for the PR, or “require passing pipelines” if your plan supports it. Until that is enabled, a red pipeline is visible but may not stop merges automatically.
246
+ **Blocking merges on failure** is configured in **Bitbucket repository
247
+ settings** (not in this repo): enable merge checks that require a successful
248
+ pull-request build—for example a minimum number of successful builds for the PR,
249
+ or “require passing pipelines” if your plan supports it. Until that is enabled,
250
+ a red pipeline is visible but may not stop merges automatically.
@@ -1,20 +1,24 @@
1
1
  # eturnityConsentBridge
2
2
 
3
- Lightweight browser bridge for synchronizing consent-related cookie payloads between a host page and an embedded Eturnity iframe via `postMessage`.
3
+ Lightweight browser bridge for synchronizing consent-related cookie payloads
4
+ between a host page and an embedded Eturnity iframe via `postMessage`.
4
5
 
5
6
  ## Overview
6
7
 
7
- `eturnityConsentBridge` is a public browser/CDN script that exposes a global function:
8
+ `eturnityConsentBridge` is a public browser/CDN script that exposes a global
9
+ function:
8
10
 
9
11
  - `window.eturnityConsentBridge(...)`
10
12
  - `eturnityConsentBridge(...)`
11
13
 
12
- It is designed for third-party embedding on customer websites and supports multiple bridge instances (multiple iframes) on the same page.
14
+ It is designed for third-party embedding on customer websites and supports
15
+ multiple bridge instances (multiple iframes) on the same page.
13
16
 
14
17
  ## Features
15
18
 
16
19
  - Global API for plain HTML and JavaScript-based script injection flows
17
- - Safe message routing per iframe instance using `event.source` and origin checks
20
+ - Safe message routing per iframe instance using `event.source` and origin
21
+ checks
18
22
  - Idempotent initialization for repeated calls with the same iframe ID
19
23
  - Non-throwing input validation (returns `null` on invalid init input)
20
24
  - Cookie fallback persistence and replay on iframe sync-ready handshake
@@ -57,7 +61,8 @@ Initialization returns:
57
61
  - instance object on success
58
62
  - `null` when validation fails
59
63
 
60
- The iframe can be missing at init time; the bridge will keep trying to resolve it during message handling.
64
+ The iframe can be missing at init time; the bridge will keep trying to resolve
65
+ it during message handling.
61
66
 
62
67
  ## Message Contract
63
68
 
@@ -88,9 +93,12 @@ Stored cookie replay can be triggered in two safe ways:
88
93
  - **on init**: right after bridge initialization (`eturnityConsentBridge(...)`)
89
94
  - **on handshake**: when iframe sends `eturnity_<type>_cookie_sync_ready`
90
95
 
91
- The init replay exists to avoid reload race conditions where the iframe handshake might be sent before the host bridge is fully attached.
96
+ The init replay exists to avoid reload race conditions where the iframe
97
+ handshake might be sent before the host bridge is fully attached.
92
98
 
93
- Delete events are also synchronized: when iframe sends `eturnity_<type>_cookie_delete`, host removes that entry from its cache and then emits `eturnity_<type>_cookie_sync` with `{ cookieName, isDeleted: true }`.
99
+ Delete events are also synchronized: when iframe sends
100
+ `eturnity_<type>_cookie_delete`, host removes that entry from its cache and then
101
+ emits `eturnity_<type>_cookie_sync` with `{ cookieName, isDeleted: true }`.
94
102
 
95
103
  ## API
96
104
 
@@ -116,7 +124,8 @@ Destroys one bridge instance.
116
124
 
117
125
  ### `eturnityConsentBridge.destroyAll()`
118
126
 
119
- Destroys all bridge instances and removes the shared `message` listener when no instances remain.
127
+ Destroys all bridge instances and removes the shared `message` listener when no
128
+ instances remain.
120
129
 
121
130
  ### `BridgeInstance.destroy()`
122
131
 
@@ -132,7 +141,8 @@ type BridgeOptions = {
132
141
  ```
133
142
 
134
143
  - **`type`** (required): selects internal message/cookie namespace
135
- - **`logs`** (optional, default `false`): enables verbose console logging for this instance
144
+ - **`logs`** (optional, default `false`): enables verbose console logging for
145
+ this instance
136
146
 
137
147
  Unknown option keys are ignored.
138
148
 
@@ -185,7 +195,8 @@ eturnityConsentBridge.destroyAll()
185
195
  ### `Iframe ... was not found during initialization`
186
196
 
187
197
  - Verify iframe ID matches exactly.
188
- - If iframe is rendered later, you can initialize early; bridge resolves context again during runtime.
198
+ - If iframe is rendered later, you can initialize early; bridge resolves context
199
+ again during runtime.
189
200
 
190
201
  ### `Could not resolve iframe origin`
191
202
 
@@ -193,15 +204,22 @@ eturnityConsentBridge.destroyAll()
193
204
 
194
205
  ### `Ignoring ... due to origin mismatch`
195
206
 
196
- - Host and iframe messaging origins must match the iframe `src` origin resolved by the bridge.
207
+ - Host and iframe messaging origins must match the iframe `src` origin resolved
208
+ by the bridge.
197
209
 
198
210
  ### Why replay may happen before `cookie_sync_ready`
199
211
 
200
- - This is expected behavior: replay on init is intentional and does not depend on handshake timing.
201
- - In some page-load orders, the iframe can emit `cookie_sync_ready` before the host bridge listener is attached; in that case you may not see a handshake log.
202
- - This does not break integration: the iframe consumes `eturnity_<type>_cookie_sync` messages directly, and `cookie_sync_ready` is only a replay signal from iframe to host.
212
+ - This is expected behavior: replay on init is intentional and does not depend
213
+ on handshake timing.
214
+ - In some page-load orders, the iframe can emit `cookie_sync_ready` before the
215
+ host bridge listener is attached; in that case you may not see a handshake
216
+ log.
217
+ - This does not break integration: the iframe consumes
218
+ `eturnity_<type>_cookie_sync` messages directly, and `cookie_sync_ready` is
219
+ only a replay signal from iframe to host.
203
220
 
204
221
  ### Cookie warning on non-HTTPS host
205
222
 
206
- - The bridge writes cookies with `Secure`; on non-HTTPS pages browsers may reject persistence.
223
+ - The bridge writes cookies with `Secure`; on non-HTTPS pages browsers may
224
+ reject persistence.
207
225
  - Use HTTPS in production embedding environments.
package/dist/index.es3.js CHANGED
@@ -815,8 +815,8 @@ const theme = (() => {
815
815
  iconBackgroundColor: semanticColors.buttonIcon.overlayWhite10
816
816
  },
817
817
  disabled: {
818
- textColor: semanticColors.grey[800],
819
- backgroundColor: semanticColors.transparentTeal[100],
818
+ textColor: semanticColors.transparentWhite[400],
819
+ backgroundColor: "transparent",
820
820
  borderColor: "",
821
821
  iconBackgroundColor: semanticColors.buttonIcon.none
822
822
  }
@@ -843,7 +843,7 @@ const theme = (() => {
843
843
  },
844
844
  disabled: {
845
845
  backgroundColor: "transparent",
846
- textColor: semanticColors.transparentRed[500],
846
+ textColor: semanticColors.transparentWhite[400],
847
847
  borderColor: "",
848
848
  iconBackgroundColor: semanticColors.buttonIcon.none
849
849
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eturnity/eturnity_reusable_components",
3
- "version": "9.37.2",
3
+ "version": "9.37.3-ATE-87.2",
4
4
  "files": [
5
5
  "dist",
6
6
  "src"
@@ -2,6 +2,7 @@
2
2
 
3
3
  colors in icons will be replaced in paths only.
4
4
 
5
- if an svg contain a rect or polygon or circle you want color to be applied on, convert them into path using:
5
+ if an svg contain a rect or polygon or circle you want color to be applied on,
6
+ convert them into path using:
6
7
 
7
8
  https://thednp.github.io/svg-path-commander/convert.html
@@ -816,8 +816,8 @@ const theme = (() => {
816
816
  iconBackgroundColor: semanticColors.buttonIcon.overlayWhite10,
817
817
  },
818
818
  disabled: {
819
- textColor: semanticColors.grey[800],
820
- backgroundColor: semanticColors.transparentTeal[100],
819
+ textColor: semanticColors.transparentWhite[400],
820
+ backgroundColor: 'transparent',
821
821
  borderColor: '',
822
822
  iconBackgroundColor: semanticColors.buttonIcon.none,
823
823
  },
@@ -844,7 +844,7 @@ const theme = (() => {
844
844
  },
845
845
  disabled: {
846
846
  backgroundColor: 'transparent',
847
- textColor: semanticColors.transparentRed[500],
847
+ textColor: semanticColors.transparentWhite[400],
848
848
  borderColor: '',
849
849
  iconBackgroundColor: semanticColors.buttonIcon.none,
850
850
  },
@@ -65,6 +65,7 @@
65
65
  display: flex;
66
66
  align-items: center;
67
67
  gap: 8px;
68
+ white-space: nowrap;
68
69
  `
69
70
 
70
71
  const Counter = styled.div`
@@ -5,7 +5,12 @@
5
5
  <TitleContainer>
6
6
  <BoxTitle> {{ numberSelected }} {{ selectedText }} </BoxTitle>
7
7
  <IconContainer @click="$emit('on-close')">
8
- <Icon color="white" cursor="pointer" name="close" size="14px" />
8
+ <Icon
9
+ :color="theme.semanticColors.red[500]"
10
+ cursor="pointer"
11
+ name="close"
12
+ size="14px"
13
+ />
9
14
  </IconContainer>
10
15
  </TitleContainer>
11
16
  <DividerContainer>
@@ -15,17 +20,18 @@
15
20
  <ExpandedListItem
16
21
  v-for="item in expandedOptions"
17
22
  :key="item.type"
23
+ data-test-id="expanded_option"
18
24
  :disabled="item.disabled || false"
19
25
  :has-component="!!item?.component"
20
- :hover-color="item.hoverColor"
26
+ :text-color="expandedItemColor(item)"
21
27
  @click="
22
28
  !item?.component && !item.disabled && $emit('on-' + item.type)
23
29
  "
24
30
  >
25
31
  <ListItemWrapper
26
32
  v-if="item?.component"
33
+ data-test-id="expanded_list_item"
27
34
  :has-component="!!item?.component"
28
- test-data-id="expanded_list_item"
29
35
  @click="!item.disabled && toggleElement(item.type)"
30
36
  >
31
37
  <ListItemTitle>
@@ -38,7 +44,6 @@
38
44
  <Icon
39
45
  :color="theme.colors.white"
40
46
  :cursor="item.disabled ? 'not-allowed' : 'pointer'"
41
- :hovered-color="theme.colors.white"
42
47
  name="arrow_right"
43
48
  size="20px"
44
49
  />
@@ -60,42 +65,44 @@
60
65
  <SelectedContainer>
61
66
  {{ numberSelected }} {{ selectedText }}
62
67
  </SelectedContainer>
63
- <ListContainer v-if="optionsList.length">
64
- <ListItem
65
- v-for="item in limitedOptions"
66
- :key="item.type"
67
- :disabled="item.disabled || false"
68
- :hover-color="item.hoverColor"
69
- @click="$emit('on-' + item.type)"
70
- >
71
- {{ item.name }}
72
- </ListItem>
73
- <IconContainer
74
- v-if="optionsList.length > optionLimit || hasComponent"
75
- test-data-id="more_actions_button_wrapper"
76
- @click="expandOptions"
77
- >
78
- <ButtonContainer name="more_options,_tool_tips">
79
- <DotItem />
80
- <DotItem />
81
- <DotItem />
82
- </ButtonContainer>
83
- </IconContainer>
84
- </ListContainer>
85
- <EmptyText v-else test-data-id="no_bulk_actions">
86
- {{ noBulkActionsText }}
87
- </EmptyText>
88
- <CloseContainer>
89
- <IconContainer @click="$emit('on-close')">
68
+ <ActionsContainer>
69
+ <template v-if="optionsList.length">
70
+ <RCMainButton
71
+ v-for="item in limitedOptions"
72
+ :key="item.type"
73
+ app-theme="dark"
74
+ :data-id="item.dataId"
75
+ :is-disabled="item.disabled"
76
+ no-wrap
77
+ :text="item.name"
78
+ type="ghost"
79
+ :variant="item.variant"
80
+ @click="!item.disabled && $emit('on-' + item.type)"
81
+ />
82
+ <IconContainer
83
+ v-if="optionsList.length > optionLimit || hasComponent"
84
+ data-test-id="more_actions_button_wrapper"
85
+ @click="expandOptions"
86
+ >
87
+ <ButtonContainer name="more_options,_tool_tips">
88
+ <DotItem />
89
+ <DotItem />
90
+ <DotItem />
91
+ </ButtonContainer>
92
+ </IconContainer>
93
+ </template>
94
+ <EmptyText v-else data-test-id="no_bulk_actions">
95
+ {{ noBulkActionsText }}
96
+ </EmptyText>
97
+ <IconContainer data-test-id="close_button" @click="$emit('on-close')">
90
98
  <Icon
91
- :color="theme.colors.white"
99
+ :color="theme.semanticColors.red[500]"
92
100
  cursor="pointer"
93
- :hovered-color="theme.colors.white"
94
101
  name="close"
95
102
  size="14px"
96
103
  />
97
104
  </IconContainer>
98
- </CloseContainer>
105
+ </ActionsContainer>
99
106
  </BoxContainer>
100
107
  </PageContainer>
101
108
  </PageWrapper>
@@ -118,7 +125,8 @@
118
125
  // {
119
126
  // type: 'delete',
120
127
  // name: 'Delete',
121
- // hoverColor: 'red' // default is green
128
+ // variant: 'cancel', // default is 'main'
129
+ // dataId: 'button_bulk_delete'
122
130
  // }
123
131
  // ]
124
132
  // @on-${type}="function" should $emit the callback for the 'type' in the optionsList
@@ -137,6 +145,7 @@
137
145
  // </SelectedOptions>
138
146
  import styled from 'vue3-styled-components'
139
147
  import InfoText from '../infoText'
148
+ import RCMainButton from '../buttons/mainButton'
140
149
  import Icon from '../icon'
141
150
  import theme from '../../assets/theme'
142
151
 
@@ -149,7 +158,7 @@
149
158
  `
150
159
 
151
160
  const Divider = styled.div`
152
- border-bottom: 1px solid rgba(255, 255, 255, 0.2);
161
+ border-bottom: 1px solid ${(props) => props.theme.semanticColors.teal[600]};
153
162
  width: 95%;
154
163
  margin: 0 auto;
155
164
  `
@@ -159,7 +168,6 @@
159
168
  bottom: 30px;
160
169
  left: 50%;
161
170
  width: auto;
162
- max-width: 70%;
163
171
  transform: translateX(-50%);
164
172
  z-index: 999;
165
173
  `
@@ -170,13 +178,12 @@
170
178
  left: 50%;
171
179
  transform: translate(-50%, -50%);
172
180
  border-radius: 4px;
173
- background-color: rgba(0, 0, 0, 0.4);
174
181
  `
175
182
 
176
183
  const OptionsContainer = styled.div`
177
184
  position: absolute;
178
185
  left: 101%;
179
- background-color: ${(props) => props.theme.colors.black};
186
+ background-color: ${(props) => props.theme.colors.transparentBlack2};
180
187
  border-radius: 4px;
181
188
  padding: 15px;
182
189
  `
@@ -185,9 +192,9 @@
185
192
  display: grid;
186
193
  align-items: center;
187
194
  height: 100%;
188
- padding-right: 20px;
195
+ padding: 0 24px;
189
196
  white-space: nowrap;
190
- border-right: 1px solid rgba(255, 255, 255, 0.2);
197
+ border-right: 1px solid ${(props) => props.theme.semanticColors.teal[600]};
191
198
  `
192
199
 
193
200
  const TitleContainer = styled.div`
@@ -210,13 +217,11 @@
210
217
  const BoxContainer = styled.div`
211
218
  display: flex;
212
219
  align-items: center;
213
- background-color: ${(props) => props.theme.colors.black};
214
- opacity: 90%;
220
+ background-color: ${(props) => props.theme.colors.transparentBlack2};
215
221
  color: ${(props) => props.theme.colors.white};
216
222
  border-radius: 4px;
217
- padding: 0px 10px 0px 20px;
218
- font-size: 13px;
219
- height: 45px;
223
+ font-size: 14px;
224
+ height: 62px;
220
225
  `
221
226
 
222
227
  const CenterBox = styled(BoxContainer)`
@@ -239,14 +244,10 @@
239
244
 
240
245
  div {
241
246
  // This is the dot color
242
- background: ${(props) => props.theme.colors.white};
247
+ background: ${(props) => props.theme.semanticColors.grey[600]};
243
248
  }
244
249
  `
245
250
 
246
- const CloseContainer = styled.div`
247
- margin-left: 20px;
248
- `
249
-
250
251
  const DotItem = styled.div`
251
252
  width: 4px;
252
253
  height: 4px;
@@ -254,38 +255,15 @@
254
255
  border-radius: 50%;
255
256
  `
256
257
 
257
- const ListContainer = styled.div`
258
+ const ActionsContainer = styled.div`
258
259
  height: 100%;
259
260
  display: flex;
260
261
  align-items: center;
262
+ gap: 8px;
263
+ padding: 0 24px;
261
264
  color: ${(props) => props.theme.colors.white};
262
265
  `
263
266
 
264
- const ListAttrs = {
265
- disabled: Boolean,
266
- hoverColor: String,
267
- }
268
-
269
- const ListItem = styled('div', ListAttrs)`
270
- width: max-content;
271
- height: 100%;
272
- display: flex;
273
- align-items: center;
274
- padding: 0 10px;
275
- cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};
276
- color: ${(props) => props.disabled && props.theme.colors.grey3};
277
-
278
- &:hover {
279
- background: rgba(255, 255, 255, 0.1);
280
- color: ${(props) =>
281
- props.disabled
282
- ? props.theme.colors.grey3
283
- : props.hoverColor
284
- ? props.theme.colors[props.hoverColor]
285
- : props.theme.colors.white};
286
- }
287
- `
288
-
289
267
  const IconContainer = styled.div`
290
268
  display: grid;
291
269
  align-items: center;
@@ -293,10 +271,9 @@
293
271
  height: 30px;
294
272
  width: 30px;
295
273
  cursor: pointer;
296
- margin-left: 8px;
297
274
 
298
275
  &:hover {
299
- background: rgba(255, 255, 255, 0.1);
276
+ background: ${(props) => props.theme.semanticColors.transparentTeal[400]};
300
277
  border-radius: 4px;
301
278
  }
302
279
  `
@@ -309,8 +286,8 @@
309
286
 
310
287
  const ExpandedListItem = styled('div', {
311
288
  disabled: Boolean,
312
- hoverColor: String,
313
289
  hasComponent: Boolean,
290
+ textColor: String,
314
291
  })`
315
292
  width: 100%;
316
293
  display: flex;
@@ -318,12 +295,10 @@
318
295
  align-items: center;
319
296
  padding: ${(props) => (props.hasComponent ? '0' : '13px 15px')};
320
297
  cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};
321
- color: ${(props) => props.disabled && props.theme.colors.grey3};
298
+ color: ${(props) => props.textColor};
322
299
 
323
300
  &:hover {
324
- background: rgba(255, 255, 255, 0.1);
325
- color: ${(props) =>
326
- props.disabled ? props.theme.colors.grey3 : props.theme.colors.white};
301
+ background: ${(props) => props.theme.semanticColors.transparentTeal[400]};
327
302
  }
328
303
  `
329
304
 
@@ -343,8 +318,7 @@
343
318
 
344
319
  const EmptyText = styled.div`
345
320
  color: ${(props) => props.theme.colors.white};
346
- font-size: 13px;
347
- padding-left: 16px;
321
+ font-size: 14px;
348
322
  `
349
323
 
350
324
  export default {
@@ -353,10 +327,10 @@
353
327
  PageContainer,
354
328
  BoxContainer,
355
329
  SelectedContainer,
356
- ListContainer,
330
+ ActionsContainer,
357
331
  ButtonContainer,
358
332
  DotItem,
359
- ListItem,
333
+ RCMainButton,
360
334
  InfoText,
361
335
  CenterBox,
362
336
  Icon,
@@ -373,7 +347,6 @@
373
347
  Divider,
374
348
  DividerContainer,
375
349
  ExpandedList,
376
- CloseContainer,
377
350
  },
378
351
  props: {
379
352
  optionsList: {
@@ -434,6 +407,13 @@
434
407
  this.initializeOptions()
435
408
  },
436
409
  methods: {
410
+ expandedItemColor(item) {
411
+ const ghost = this.theme.mainButton.dark.ghost
412
+ const variant = ghost[item.variant] ?? ghost.main
413
+ const state = item.disabled ? 'disabled' : 'default'
414
+
415
+ return variant[state].textColor
416
+ },
437
417
  initializeOptions() {
438
418
  this.expandedOptions = [...this.optionsList]
439
419
  .map((option, index) => {
@@ -457,17 +437,6 @@
457
437
  this.expanded = !this.expanded
458
438
  this.expandedOptions = this.optionsList
459
439
  },
460
- handleExpandedOptionClick(item) {
461
- if (item.disabled) return
462
-
463
- if (!item.component) {
464
- this.$emit('on-' + item.type)
465
-
466
- return
467
- }
468
-
469
- this.toggleElement(item.type)
470
- },
471
440
  },
472
441
  }
473
442
  </script>
@@ -1,6 +1,8 @@
1
1
  /* eslint-disable */
2
2
  import { mount } from '@vue/test-utils'
3
3
  import RCSelectedOptions from '@/components/selectedOptions'
4
+ import RCMainButton from '@/components/buttons/mainButton'
5
+ import RCIcon from '@/components/icon/Icon.vue'
4
6
  import theme from '@/assets/theme'
5
7
 
6
8
  jest.mock('@/components/icon/iconCache.mjs', () => ({
@@ -22,7 +24,7 @@ describe('RCSelectedOptions.vue', () => {
22
24
  {
23
25
  type: 'delete',
24
26
  name: 'Delete',
25
- hoverColor: 'red',
27
+ variant: 'cancel',
26
28
  },
27
29
  ],
28
30
  }
@@ -58,7 +60,7 @@ describe('RCSelectedOptions.vue', () => {
58
60
  expect(selectedOptions.exists()).toBe(true)
59
61
 
60
62
  const noBulkActions = selectedOptions.find(
61
- '[test-data-id="no_bulk_actions"]'
63
+ '[data-test-id="no_bulk_actions"]'
62
64
  )
63
65
  expect(noBulkActions.exists()).toBe(true)
64
66
  })
@@ -78,7 +80,7 @@ describe('RCSelectedOptions.vue', () => {
78
80
 
79
81
  //since we have 2 options, and the limit is 4, we should not see "•••" button
80
82
  const moreActionsButton = selectedOptions.find(
81
- '[test-data-id="more_actions_button_wrapper"]'
83
+ '[data-test-id="more_actions_button_wrapper"]'
82
84
  )
83
85
  expect(moreActionsButton.exists()).toBe(false)
84
86
  })
@@ -111,7 +113,7 @@ describe('RCSelectedOptions.vue', () => {
111
113
 
112
114
  //since we a component parameter, we should see "•••" button
113
115
  const moreActionsButton = selectedOptions.find(
114
- '[test-data-id="more_actions_button_wrapper"]'
116
+ '[data-test-id="more_actions_button_wrapper"]'
115
117
  )
116
118
  expect(moreActionsButton.exists()).toBe(true)
117
119
  })
@@ -146,7 +148,7 @@ describe('RCSelectedOptions.vue', () => {
146
148
 
147
149
  //since we a component parameter, we should see "•••" button
148
150
  const moreActionsButton = selectedOptions.find(
149
- '[test-data-id="more_actions_button_wrapper"]'
151
+ '[data-test-id="more_actions_button_wrapper"]'
150
152
  )
151
153
  expect(moreActionsButton.exists()).toBe(true)
152
154
 
@@ -160,7 +162,7 @@ describe('RCSelectedOptions.vue', () => {
160
162
 
161
163
  //toggle the element with the component parameter
162
164
  const expandedListItems = selectedOptions.findAll(
163
- '[test-data-id="expanded_list_item"]'
165
+ '[data-test-id="expanded_list_item"]'
164
166
  )
165
167
 
166
168
  //there should be only one since we have only one option with the component parameter
@@ -173,4 +175,239 @@ describe('RCSelectedOptions.vue', () => {
173
175
  const setSupplierSlot = wrapper.find('[data-id="set_supplier"]')
174
176
  expect(setSupplierSlot.exists()).toBe(true)
175
177
  })
178
+ it('renders each collapsed option as a main button', async () => {
179
+ const wrapper = mount(RCSelectedOptions, {
180
+ props: simpleOptionsProps,
181
+ global: {
182
+ provide: {
183
+ theme,
184
+ },
185
+ },
186
+ })
187
+
188
+ await wrapper.vm.$nextTick()
189
+
190
+ const optionButtons = wrapper.findAllComponents(RCMainButton)
191
+ expect(optionButtons).toHaveLength(2)
192
+ expect(optionButtons[0].props('text')).toBe('Close')
193
+ expect(optionButtons[1].props('text')).toBe('Delete')
194
+ })
195
+
196
+ it('passes the option data id straight through to the button', async () => {
197
+ const wrapper = mount(RCSelectedOptions, {
198
+ props: {
199
+ ...simpleOptionsProps,
200
+ optionsList: [
201
+ { type: 'delete', name: 'Delete', dataId: 'button_bulk_delete' },
202
+ ],
203
+ },
204
+ global: {
205
+ provide: {
206
+ theme,
207
+ },
208
+ },
209
+ })
210
+
211
+ await wrapper.vm.$nextTick()
212
+
213
+ expect(wrapper.findComponent(RCMainButton).props('dataId')).toBe(
214
+ 'button_bulk_delete'
215
+ )
216
+ })
217
+
218
+ it('leaves the button without a data id when the option carries none', async () => {
219
+ const wrapper = mount(RCSelectedOptions, {
220
+ props: simpleOptionsProps,
221
+ global: {
222
+ provide: {
223
+ theme,
224
+ },
225
+ },
226
+ })
227
+
228
+ await wrapper.vm.$nextTick()
229
+
230
+ expect(wrapper.findComponent(RCMainButton).props('dataId')).toBe('')
231
+ })
232
+
233
+ it('passes the option variant straight through to the button', async () => {
234
+ const wrapper = mount(RCSelectedOptions, {
235
+ props: simpleOptionsProps,
236
+ global: {
237
+ provide: {
238
+ theme,
239
+ },
240
+ },
241
+ })
242
+
243
+ await wrapper.vm.$nextTick()
244
+
245
+ const optionButtons = wrapper.findAllComponents(RCMainButton)
246
+ expect(optionButtons[0].props('variant')).toBe('main')
247
+ expect(optionButtons[1].props('variant')).toBe('cancel')
248
+ })
249
+
250
+ it('marks a disabled option as a disabled button and does not emit on click', async () => {
251
+ const wrapper = mount(RCSelectedOptions, {
252
+ props: {
253
+ ...simpleOptionsProps,
254
+ optionsList: [
255
+ {
256
+ type: 'delete',
257
+ name: 'Delete',
258
+ disabled: true,
259
+ },
260
+ ],
261
+ },
262
+ global: {
263
+ provide: {
264
+ theme,
265
+ },
266
+ },
267
+ })
268
+
269
+ await wrapper.vm.$nextTick()
270
+
271
+ const deleteButton = wrapper.findComponent(RCMainButton)
272
+ expect(deleteButton.props('isDisabled')).toBe(true)
273
+
274
+ await deleteButton.trigger('click')
275
+ expect(wrapper.emitted('on-delete')).toBeUndefined()
276
+ })
277
+
278
+ it('emits the option event when an enabled option is clicked', async () => {
279
+ const wrapper = mount(RCSelectedOptions, {
280
+ props: simpleOptionsProps,
281
+ global: {
282
+ provide: {
283
+ theme,
284
+ },
285
+ },
286
+ })
287
+
288
+ await wrapper.vm.$nextTick()
289
+
290
+ await wrapper.findAllComponents(RCMainButton)[1].trigger('click')
291
+ expect(wrapper.emitted('on-delete')).toHaveLength(1)
292
+ })
293
+ it('renders the close button and emits on-close even when there are no options', async () => {
294
+ const wrapper = mount(RCSelectedOptions, {
295
+ props: {
296
+ ...simpleOptionsProps,
297
+ optionsList: [],
298
+ },
299
+ global: {
300
+ provide: {
301
+ theme,
302
+ },
303
+ },
304
+ })
305
+
306
+ await wrapper.vm.$nextTick()
307
+
308
+ const closeButton = wrapper.find('[data-test-id="close_button"]')
309
+ expect(closeButton.exists()).toBe(true)
310
+
311
+ await closeButton.trigger('click')
312
+ expect(wrapper.emitted('on-close')).toHaveLength(1)
313
+ })
314
+
315
+ const expandedItemColors = async (optionsList) => {
316
+ const wrapper = mount(RCSelectedOptions, {
317
+ props: {
318
+ ...simpleOptionsProps,
319
+ optionsList,
320
+ },
321
+ global: {
322
+ provide: {
323
+ theme,
324
+ },
325
+ },
326
+ })
327
+
328
+ await wrapper
329
+ .find('[data-test-id="more_actions_button_wrapper"]')
330
+ .trigger('click')
331
+ await wrapper.vm.$nextTick()
332
+
333
+ return wrapper
334
+ .findAllComponents('[data-test-id="expanded_option"]')
335
+ .map((option) => option.props('textColor'))
336
+ }
337
+
338
+ it('styles a cancel option differently from the default options in the expanded list', async () => {
339
+ const [close, archive, remove] = await expandedItemColors([
340
+ { type: 'close', name: 'Close' },
341
+ { type: 'archive', name: 'Archive' },
342
+ { type: 'delete', name: 'Delete', variant: 'cancel' },
343
+ { type: 'supplier', name: 'Supplier', component: 'set_supplier' },
344
+ ])
345
+
346
+ expect(archive).toBe(close)
347
+ expect(remove).not.toBe(close)
348
+ })
349
+
350
+ it('styles disabled options alike whatever the variant in the expanded list', async () => {
351
+ const [close, remove] = await expandedItemColors([
352
+ { type: 'close', name: 'Close', disabled: true },
353
+ { type: 'delete', name: 'Delete', variant: 'cancel', disabled: true },
354
+ { type: 'supplier', name: 'Supplier', component: 'set_supplier' },
355
+ ])
356
+
357
+ expect(remove).toBe(close)
358
+ })
359
+
360
+ it('takes expanded option colours from the ghost button theme', async () => {
361
+ const [close, remove, archive, purge] = await expandedItemColors([
362
+ { type: 'close', name: 'Close' },
363
+ { type: 'delete', name: 'Delete', variant: 'cancel' },
364
+ { type: 'archive', name: 'Archive', disabled: true },
365
+ { type: 'purge', name: 'Purge', variant: 'cancel', disabled: true },
366
+ { type: 'supplier', name: 'Supplier', component: 'set_supplier' },
367
+ ])
368
+
369
+ const ghost = theme.mainButton.dark.ghost
370
+
371
+ expect(close).toBe(ghost.main.default.textColor)
372
+ expect(remove).toBe(ghost.cancel.default.textColor)
373
+ expect(archive).toBe(ghost.main.disabled.textColor)
374
+ expect(purge).toBe(ghost.cancel.disabled.textColor)
375
+ })
376
+
377
+ it('uses the same close colour in the expanded modal as in the collapsed bar', async () => {
378
+ const wrapper = mount(RCSelectedOptions, {
379
+ props: {
380
+ ...simpleOptionsProps,
381
+ optionsList: [
382
+ { type: 'close', name: 'Close' },
383
+ { type: 'supplier', name: 'Supplier', component: 'set_supplier' },
384
+ ],
385
+ },
386
+ global: {
387
+ provide: {
388
+ theme,
389
+ },
390
+ },
391
+ })
392
+
393
+ await wrapper
394
+ .find('[data-test-id="more_actions_button_wrapper"]')
395
+ .trigger('click')
396
+ await wrapper.vm.$nextTick()
397
+
398
+ const closeIcon = wrapper
399
+ .findAllComponents(RCIcon)
400
+ .find((icon) => icon.props('name') === 'close')
401
+
402
+ expect(closeIcon.props('color')).toBe(theme.semanticColors.red[500])
403
+ })
404
+
405
+ it('falls back to the main variant when an option has an unknown variant', async () => {
406
+ const [mystery] = await expandedItemColors([
407
+ { type: 'mystery', name: 'Mystery', variant: 'not_a_variant' },
408
+ { type: 'supplier', name: 'Supplier', component: 'set_supplier' },
409
+ ])
410
+
411
+ expect(mystery).toBe(theme.mainButton.dark.ghost.main.default.textColor)
412
+ })
176
413
  })
@@ -9,7 +9,7 @@ export default {
9
9
  optionsList: {
10
10
  control: 'object',
11
11
  description:
12
- 'Array of options to display. Each option should have a type and name, and can optionally have a component, disabled state, hoverColor, and disabledInfo.',
12
+ 'Array of options to display. Each option should have a type and name, and can optionally have a component, disabled state, variant, dataId, and disabledInfo.',
13
13
  },
14
14
  numberSelected: {
15
15
  control: 'number',
@@ -49,7 +49,7 @@ Default.args = {
49
49
  {
50
50
  type: 'delete',
51
51
  name: 'Delete',
52
- hoverColor: 'red',
52
+ variant: 'cancel',
53
53
  },
54
54
  ],
55
55
  numberSelected: 5,
@@ -98,7 +98,7 @@ ManyOptions.args = {
98
98
  {
99
99
  type: 'delete',
100
100
  name: 'Delete',
101
- hoverColor: 'red',
101
+ variant: 'cancel',
102
102
  },
103
103
  {
104
104
  type: 'archive',
@@ -132,7 +132,7 @@ CustomLabels.args = {
132
132
  {
133
133
  type: 'delete',
134
134
  name: 'Delete',
135
- hoverColor: 'red',
135
+ variant: 'cancel',
136
136
  },
137
137
  ],
138
138
  numberSelected: 1,
@@ -158,7 +158,8 @@ CustomLabels.args = {
158
158
  // {
159
159
  // type: 'delete',
160
160
  // name: 'Delete',
161
- // hoverColor: 'red' // default is green
161
+ // variant: 'cancel', // default is 'main'
162
+ // dataId: 'button_bulk_delete'
162
163
  // }
163
164
  // ]
164
165
  //
@@ -1,3 +0,0 @@
1
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <path d="M7.99988 2C9.38419 2 10.7375 2.41068 11.8885 3.17969C13.0397 3.94883 13.9369 5.04227 14.4667 6.32129C14.9964 7.60027 15.1351 9.00747 14.8651 10.3652C14.6292 11.551 14.0911 12.6534 13.3065 13.5654L13.2294 13.6367C13.0359 13.7809 12.7574 13.7575 12.578 13.5781C12.3736 13.3731 12.3756 13.0424 12.5614 12.8203C13.1208 12.1523 13.5272 11.3687 13.7509 10.5254L13.8358 10.1611C14.0654 9.00695 13.9473 7.80988 13.4969 6.72266C13.0747 5.70353 12.3778 4.82341 11.4862 4.17871L11.3055 4.05273C10.3271 3.399 9.17662 3.0498 7.99988 3.0498C6.82316 3.04985 5.67263 3.39898 4.69421 4.05273C3.71591 4.70646 2.95314 5.63563 2.50281 6.72266L2.42273 6.92773C2.03943 7.95934 1.94869 9.07895 2.16394 10.1611C2.3593 11.143 2.79919 12.0571 3.43835 12.8203C3.62426 13.0425 3.62633 13.3731 3.42175 13.5781L3.33875 13.6455C3.16397 13.7608 2.93614 13.7604 2.77039 13.6367L2.69324 13.5654C2.00668 12.7673 1.50892 11.8234 1.23718 10.8057L1.13464 10.3652C0.881519 9.09233 0.987587 7.77593 1.43835 6.5625L1.53308 6.32129C2.02976 5.12224 2.84939 4.08667 3.89832 3.32812L4.11121 3.17969C5.26224 2.41066 6.61558 2.00004 7.99988 2ZM10.1093 5.1875C10.2817 4.97192 10.5968 4.93699 10.8124 5.10938C11.028 5.28188 11.063 5.59687 10.8905 5.8125L9.12781 8.01465C9.35847 8.27842 9.49988 8.62208 9.49988 9C9.49988 9.82843 8.82831 10.5 7.99988 10.5C7.17154 10.4999 6.49988 9.82836 6.49988 9C6.49988 8.17164 7.17154 7.50011 7.99988 7.5C8.08232 7.5 8.16324 7.50771 8.24207 7.52051L10.1093 5.1875ZM7.99988 8.5C7.72383 8.50011 7.49988 8.72392 7.49988 9C7.49988 9.27608 7.72383 9.49989 7.99988 9.5C8.27602 9.5 8.49988 9.27614 8.49988 9C8.49988 8.72386 8.27602 8.5 7.99988 8.5Z" fill="#263238"/>
3
- </svg>