@djangocfg/ui-core 2.1.547 → 2.1.549

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-core",
3
- "version": "2.1.547",
3
+ "version": "2.1.549",
4
4
  "description": "Pure React UI component library without Next.js dependencies - for Electron, Vite, CRA apps",
5
5
  "keywords": [
6
6
  "ui-components",
@@ -130,7 +130,7 @@
130
130
  "check:contrast": "node scripts/check-preset-contrast.mjs"
131
131
  },
132
132
  "peerDependencies": {
133
- "@djangocfg/i18n": "^2.1.547",
133
+ "@djangocfg/i18n": "^2.1.549",
134
134
  "consola": "^3.4.2",
135
135
  "lucide-react": "^0.545.0",
136
136
  "moment": "^2.30.1",
@@ -206,9 +206,9 @@
206
206
  "vaul": "1.1.2"
207
207
  },
208
208
  "devDependencies": {
209
- "@djangocfg/eslint-config": "^2.1.547",
210
- "@djangocfg/i18n": "^2.1.547",
211
- "@djangocfg/typescript-config": "^2.1.547",
209
+ "@djangocfg/eslint-config": "^2.1.549",
210
+ "@djangocfg/i18n": "^2.1.549",
211
+ "@djangocfg/typescript-config": "^2.1.549",
212
212
  "@storybook/react-vite": "^10.5.0",
213
213
  "@types/node": "^24.13.3",
214
214
  "@types/react": "19.2.15",
@@ -84,20 +84,25 @@ const PhoneInput = React.forwardRef<HTMLInputElement, PhoneInputProps>(
84
84
  )
85
85
  }, [searchQuery])
86
86
 
87
- // Initialize input value from props
87
+ // Mirror the controlled value, empty included. Skipping the empty case
88
+ // leaves the last number on screen after a parent resets the form, so the
89
+ // field shows digits the form no longer holds.
88
90
  React.useEffect(() => {
89
- if (value) {
90
- try {
91
- const phoneNumber = parsePhoneNumberFromString(value)
92
- if (phoneNumber) {
93
- setSelectedCountry(phoneNumber.country || defaultCountry)
94
- setInputValue(phoneNumber.nationalNumber)
95
- } else {
96
- setInputValue(value)
97
- }
98
- } catch {
91
+ if (!value) {
92
+ setInputValue('')
93
+ setSelectedCountry(defaultCountry)
94
+ return
95
+ }
96
+ try {
97
+ const phoneNumber = parsePhoneNumberFromString(value)
98
+ if (phoneNumber) {
99
+ setSelectedCountry(phoneNumber.country || defaultCountry)
100
+ setInputValue(phoneNumber.nationalNumber)
101
+ } else {
99
102
  setInputValue(value)
100
103
  }
104
+ } catch {
105
+ setInputValue(value)
101
106
  }
102
107
  }, [value, defaultCountry])
103
108
 
@@ -242,7 +242,9 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
242
242
  badgeAnimation: animationConfig?.badgeAnimation || (animation > 0 ? "bounce" : "none"),
243
243
  popoverAnimation: animationConfig?.popoverAnimation || "scale",
244
244
  optionHoverAnimation: animationConfig?.optionHoverAnimation || "highlight",
245
- duration: animationConfig?.duration || animation || 0.3,
245
+ // `??`: a caller asking for duration 0 means "no animation",
246
+ // and `||` swallowed it into the 0.3 default.
247
+ duration: animationConfig?.duration ?? (animation > 0 ? animation : 0.3),
246
248
  delay: animationConfig?.delay || 0,
247
249
  }),
248
250
  [animation, animationConfig]
@@ -263,9 +265,23 @@ export const MultiSelectProAsync = React.forwardRef<MultiSelectProAsyncRef, Mult
263
265
  }, [defaultValueKey, resetOnDefaultValueChange])
264
266
 
265
267
  // Announce changes for screen readers
268
+ // One timer, cleared on unmount: the announcement is transient UI state,
269
+ // and stacking a timer per change clears it early for the latest message.
270
+ const announceTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
271
+ React.useEffect(
272
+ () => () => {
273
+ if (announceTimerRef.current) clearTimeout(announceTimerRef.current)
274
+ },
275
+ []
276
+ )
277
+
266
278
  const announce = React.useCallback((message: string) => {
267
279
  setAnnouncements(message)
268
- setTimeout(() => setAnnouncements(""), 1000)
280
+ if (announceTimerRef.current) clearTimeout(announceTimerRef.current)
281
+ announceTimerRef.current = setTimeout(() => {
282
+ announceTimerRef.current = null
283
+ setAnnouncements("")
284
+ }, 1000)
269
285
  }, [])
270
286
 
271
287
  // Toggle selection
@@ -233,7 +233,9 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
233
233
  badgeAnimation: animationConfig?.badgeAnimation || (animation > 0 ? "bounce" : "none"),
234
234
  popoverAnimation: animationConfig?.popoverAnimation || "scale",
235
235
  optionHoverAnimation: animationConfig?.optionHoverAnimation || "highlight",
236
- duration: animationConfig?.duration || animation || 0.3,
236
+ // `??`: a caller asking for duration 0 means "no animation",
237
+ // and `||` swallowed it into the 0.3 default.
238
+ duration: animationConfig?.duration ?? (animation > 0 ? animation : 0.3),
237
239
  delay: animationConfig?.delay || 0,
238
240
  }),
239
241
  [animation, animationConfig]
@@ -247,9 +249,23 @@ export const MultiSelectPro = React.forwardRef<MultiSelectProRef, MultiSelectPro
247
249
  }, [defaultValue, resetOnDefaultValueChange])
248
250
 
249
251
  // Announce changes for screen readers
252
+ // One timer, cleared on unmount: the announcement is transient UI state,
253
+ // and stacking a timer per change clears it early for the latest message.
254
+ const announceTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
255
+ React.useEffect(
256
+ () => () => {
257
+ if (announceTimerRef.current) clearTimeout(announceTimerRef.current)
258
+ },
259
+ []
260
+ )
261
+
250
262
  const announce = React.useCallback((message: string) => {
251
263
  setAnnouncements(message)
252
- setTimeout(() => setAnnouncements(""), 1000)
264
+ if (announceTimerRef.current) clearTimeout(announceTimerRef.current)
265
+ announceTimerRef.current = setTimeout(() => {
266
+ announceTimerRef.current = null
267
+ setAnnouncements("")
268
+ }, 1000)
253
269
  }, [])
254
270
 
255
271
  // Toggle selection
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useEffect } from 'react';
3
+ import { useEffect, useRef } from 'react';
4
4
 
5
5
  import { useDeviceDetect } from '../device/useDeviceDetect';
6
6
 
@@ -95,12 +95,20 @@ function releaseLock() {
95
95
  export function useBodyScrollLock(locked: boolean): void {
96
96
  const device = useDeviceDetect();
97
97
 
98
+ // `isIOS` starts false and flips once the UA parse resolves. With it in the
99
+ // deps the effect re-ran on that flip: the cleanup dropped the shared
100
+ // lockCount to 0 and released the lock — unlocking the page under a second
101
+ // overlay that was still open. The lock strategy is chosen once, on the
102
+ // acquisition that takes the count from 0 to 1.
103
+ const isIOSRef = useRef(device.isIOS);
104
+ isIOSRef.current = device.isIOS;
105
+
98
106
  useEffect(() => {
99
107
  if (!locked) return;
100
108
  if (typeof document === 'undefined') return;
101
109
 
102
110
  if (lockCount === 0) {
103
- applyLock(device.isIOS);
111
+ applyLock(isIOSRef.current);
104
112
  }
105
113
  lockCount += 1;
106
114
 
@@ -110,5 +118,5 @@ export function useBodyScrollLock(locked: boolean): void {
110
118
  releaseLock();
111
119
  }
112
120
  };
113
- }, [locked, device.isIOS]);
121
+ }, [locked]);
114
122
  }