@descope/web-components-ui 3.14.9 → 3.15.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.
@@ -174,6 +174,13 @@ class PasscodeInternal extends BaseInputClass {
174
174
 
175
175
  const debouncedHandleParseInput = debounce(handleParseInput, 0, { trailing: true });
176
176
 
177
+ // Per-input flag: set in keydown, consumed in the input event.
178
+ // Fallback for WebKit/Safari, where the native input event can fire
179
+ // non-composed; proxyInputMixin then re-dispatches it as a plain Event
180
+ // (no inputType), so the 'deleteContentBackward' check misses the
181
+ // backspace and navigation is skipped.
182
+ let lastKeyWasBackspace = false;
183
+
177
184
  // sanitize the input
178
185
  input.addEventListener('input', (e) => {
179
186
  input.value = sanitizeStr(input.value);
@@ -182,29 +189,50 @@ class PasscodeInternal extends BaseInputClass {
182
189
  toggleMaskVisibility(input, input.value[0]);
183
190
  }
184
191
 
185
- setTimeout(() => {
186
- if (e?.inputType === 'deleteContentBackward') {
187
- focusElement(this.getPrevInput(input));
188
- }
189
- });
192
+ // inputType covers iOS (unreliable keydown); the flag covers WebKit
193
+ // (missing inputType). See flag declaration above.
194
+ const isBackspace = e?.inputType === 'deleteContentBackward' || lastKeyWasBackspace;
195
+ lastKeyWasBackspace = false;
196
+
197
+ if (isBackspace) {
198
+ // Navigate synchronously (no setTimeout) to prevent WebKit key-event
199
+ // leakage: with setTimeout(0), WebKit can dispatch the next key-repeat
200
+ // backspace event to the newly focused element, deleting a second digit.
201
+ // Calling focusElement here (inside the input event, not keydown) is safe
202
+ // because the browser has already finished processing the deletion.
203
+ focusElement(this.getPrevInput(input));
204
+ }
205
+
190
206
  debouncedHandleParseInput(input.value);
191
207
  });
192
208
 
193
209
  // we want backspace to focus on the previous digit
194
- input.onkeydown = ({ key }) => {
195
- // when user deletes a digit, we want to focus the previous digit
210
+ input.onkeydown = (e) => {
211
+ const { key } = e;
196
212
  if (key === 'Backspace') {
197
- // if value is empty then the input element does not fire `input` event
198
- // if this is the case, we focus the element here.
199
- // otherwise, the focusElement occurs as part of the `input` event listener
200
213
  if (!input.value) {
201
- setTimeout(() => focusElement(this.getPrevInput(input)), 0);
214
+ // Empty field: the input event will not fire (nothing to delete),
215
+ // so we must handle navigation here.
216
+ // e.preventDefault() stops WebKit from leaking this keypress to
217
+ // the element that receives focus (key-event leakage).
218
+ e.preventDefault();
219
+ lastKeyWasBackspace = false;
220
+ focusElement(this.getPrevInput(input));
202
221
  } else {
222
+ // Non-empty field: let the browser delete normally so the input
223
+ // event fires (needed on older iOS where keydown is unreliable).
224
+ // Move the caret to the end first so Backspace deletes the digit
225
+ // even if a direct tap left the caret before it (caret at index 0).
203
226
  input.setSelectionRange(1, 1);
227
+ // Fallback signal for the input handler (see flag declaration above).
228
+ lastKeyWasBackspace = true;
229
+ }
230
+ } else {
231
+ lastKeyWasBackspace = false;
232
+ if (key.length === 1) {
233
+ // we want only characters and not command keys
234
+ input.value = ''; // we are clearing the previous value so we can override it with the new value
204
235
  }
205
- } else if (key.length === 1) {
206
- // we want only characters and not command keys
207
- input.value = ''; // we are clearing the previous value so we can override it with the new value
208
236
  }
209
237
  };
210
238
 
@@ -13,7 +13,7 @@ const template = `
13
13
  </descope-container>
14
14
  </descope-container>`;
15
15
 
16
- const Template = ({ opened }) => {
16
+ const Template = ({ opened, closeOnOutsideClick }) => {
17
17
  const loader = `
18
18
  <descope-container direction="column" st-align-items="center" st-horizontal-padding="1.25rem" st-vertical-padding="1.25rem" st-gap="0.75rem" st-background-color="transparent">
19
19
  <descope-loader-radial size="sm" mode="primary"></descope-loader-radial>
@@ -21,7 +21,7 @@ const Template = ({ opened }) => {
21
21
  `;
22
22
 
23
23
  return `
24
- <descope-modal opened="${opened}">
24
+ <descope-modal opened="${opened}" close-on-outside-click="${closeOnOutsideClick}">
25
25
  ${loader}
26
26
  </descope-modal>
27
27
  `;
@@ -30,6 +30,16 @@ const Template = ({ opened }) => {
30
30
  export default {
31
31
  component: componentName,
32
32
  title: 'descope-modal',
33
+ argTypes: {
34
+ opened: {
35
+ name: 'Opened',
36
+ control: { type: 'boolean' },
37
+ },
38
+ closeOnOutsideClick: {
39
+ name: 'Close on outside click',
40
+ control: { type: 'boolean' },
41
+ },
42
+ },
33
43
  decorators: [
34
44
  (render) => {
35
45
  setTimeout(() => {
@@ -45,4 +55,5 @@ export const Default = Template.bind({});
45
55
 
46
56
  Default.args = {
47
57
  opened: true,
58
+ closeOnOutsideClick: true,
48
59
  };