pesona-ui 1.0.28 → 1.0.29

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/dist/index.esm.js CHANGED
@@ -8187,53 +8187,120 @@ const useDropdownPositionAndScroll = (isOpen, dropdownOptionsRef) => {
8187
8187
  }, [isOpen, dropdownOptionsRef]);
8188
8188
  };
8189
8189
 
8190
- const Select = forwardRef(({ name, label, message, selectLabel, size = 'md', floatingLabel = false, error, options, value = '', onChange, required, disabled, }, ref) => {
8190
+ const Select = forwardRef(({ name, label, message, selectLabel, size = 'md', floatingLabel = false, error, options, value = '', onChange, onBlur, required, disabled, }, ref) => {
8191
8191
  const [isOpen, setIsOpen] = useState(false);
8192
+ const [highlightIndex, setHighlightIndex] = useState(-1);
8192
8193
  const dropdownRef = useRef(null);
8193
8194
  const dropdownOptionsRef = useRef(null);
8194
- const handleOptionClick = (optionValue) => {
8195
+ const hiddenSelectRef = useRef(null);
8196
+ const shouldReturnNumber = useMemo(() => options.every((opt) => typeof opt.value === 'number'), [options]);
8197
+ const coerceValue = useCallback((val) => (shouldReturnNumber ? Number(val) : val), [shouldReturnNumber]);
8198
+ const createSyntheticEvent = useCallback((val) => ({
8199
+ target: { name, value: coerceValue(val), type: 'select-one' },
8200
+ currentTarget: { name, value: coerceValue(val), type: 'select-one' },
8201
+ }), [name, coerceValue]);
8202
+ const handleOptionSelect = useCallback((optionValue) => {
8195
8203
  const selectedOption = options.find((opt) => opt.value === optionValue);
8196
- // Gunakan nilai asli sesuai tipe (number/string)
8197
- const actualValue = typeof selectedOption?.value === 'number'
8198
- ? Number(selectedOption.value)
8199
- : selectedOption?.value;
8200
- if (onChange) {
8201
- onChange({
8202
- target: {
8203
- name,
8204
- value: actualValue,
8205
- },
8206
- });
8207
- }
8204
+ if (!selectedOption)
8205
+ return;
8206
+ hiddenSelectRef.current.value = String(selectedOption.value);
8207
+ onChange?.(createSyntheticEvent(selectedOption.value));
8208
8208
  setIsOpen(false);
8209
- };
8210
- useOutsideClick([dropdownRef], () => {
8209
+ setTimeout(() => {
8210
+ onBlur?.(createSyntheticEvent(selectedOption.value));
8211
+ }, 0);
8212
+ }, [options, onChange, onBlur, createSyntheticEvent]);
8213
+ const handleOutsideClick = useCallback(() => {
8211
8214
  setIsOpen(false);
8212
- });
8215
+ if (onBlur && value !== undefined && value !== null && value !== '') {
8216
+ setTimeout(() => {
8217
+ onBlur(createSyntheticEvent(value));
8218
+ }, 0);
8219
+ }
8220
+ }, [value, onBlur, createSyntheticEvent]);
8221
+ useOutsideClick([dropdownRef], handleOutsideClick);
8213
8222
  useDropdownPositionAndScroll(isOpen, dropdownOptionsRef);
8223
+ const normalizedValue = useMemo(() => (value === null || value === undefined || value === '' ? '' : String(value)), [value]);
8224
+ const selectedOption = useMemo(() => options.find((opt) => String(opt.value) === normalizedValue), [options, normalizedValue]);
8225
+ const displayText = selectedOption?.label || selectLabel || 'Select an option';
8214
8226
  useEffect(() => {
8215
- return () => {
8227
+ if (hiddenSelectRef.current)
8228
+ hiddenSelectRef.current.value = normalizedValue;
8229
+ }, [normalizedValue]);
8230
+ useEffect(() => {
8231
+ if (isOpen) {
8232
+ const index = options.findIndex((opt) => String(opt.value) === normalizedValue);
8233
+ setHighlightIndex(index >= 0 ? index : 0);
8234
+ }
8235
+ }, [isOpen, options, normalizedValue]);
8236
+ const handleKeyDown = useCallback((e) => {
8237
+ if (disabled)
8238
+ return;
8239
+ if (e.key === 'Enter' || e.key === ' ') {
8240
+ e.preventDefault();
8241
+ if (isOpen && highlightIndex >= 0) {
8242
+ handleOptionSelect(options[highlightIndex].value);
8243
+ }
8244
+ else {
8245
+ setIsOpen((prev) => !prev);
8246
+ }
8247
+ }
8248
+ else if (e.key === 'Escape' && isOpen) {
8216
8249
  setIsOpen(false);
8217
- };
8218
- }, []);
8219
- const displayText = value
8220
- ? options.find((option) => option.value === value)?.label
8221
- : selectLabel || 'Select an option';
8250
+ }
8251
+ else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
8252
+ e.preventDefault();
8253
+ if (!isOpen) {
8254
+ setIsOpen(true);
8255
+ }
8256
+ else {
8257
+ setHighlightIndex((prev) => {
8258
+ const next = e.key === 'ArrowDown'
8259
+ ? (prev + 1) % options.length
8260
+ : (prev - 1 + options.length) % options.length;
8261
+ return next;
8262
+ });
8263
+ }
8264
+ }
8265
+ }, [disabled, isOpen, highlightIndex, options, handleOptionSelect]);
8266
+ useEffect(() => {
8267
+ if (isOpen && dropdownOptionsRef.current && highlightIndex >= 0) {
8268
+ const optionElements = dropdownOptionsRef.current.querySelectorAll('.option');
8269
+ const highlighted = optionElements[highlightIndex];
8270
+ highlighted?.scrollIntoView({ block: 'nearest' });
8271
+ }
8272
+ }, [highlightIndex, isOpen]);
8222
8273
  return (React.createElement(React.Fragment, null,
8274
+ React.createElement("select", { ref: (el) => {
8275
+ hiddenSelectRef.current = el;
8276
+ if (typeof ref === 'function')
8277
+ ref(el);
8278
+ else if (ref)
8279
+ ref.current = el;
8280
+ }, name: name, value: normalizedValue, onChange: onChange, onBlur: onBlur, required: required, disabled: disabled, style: {
8281
+ position: 'absolute',
8282
+ left: '-9999px',
8283
+ opacity: 0,
8284
+ pointerEvents: 'none',
8285
+ width: '1px',
8286
+ height: '1px',
8287
+ }, tabIndex: -1, "aria-hidden": "true" },
8288
+ React.createElement("option", { value: "" }, "Select an option"),
8289
+ options.map((option) => (React.createElement("option", { key: option.value, value: option.value }, option.label)))),
8223
8290
  label && !floatingLabel && (React.createElement("label", { htmlFor: name },
8224
8291
  label,
8225
8292
  " ",
8226
8293
  required && React.createElement("span", { className: "text-danger" }, "*"))),
8227
8294
  React.createElement("div", { className: `dropdown-select-container ${size}`, ref: dropdownRef },
8228
- React.createElement("div", { className: `dropdown-header ${isOpen ? 'open' : ''} ${disabled ? 'disabled' : ''}`, onClick: () => !disabled && setIsOpen(!isOpen), ref: ref, "aria-haspopup": "listbox", "aria-expanded": isOpen },
8295
+ React.createElement("div", { className: `dropdown-header ${isOpen ? 'open' : ''} ${disabled ? 'disabled' : ''} ${error ? 'error' : ''}`, onClick: () => !disabled && setIsOpen(!isOpen), onKeyDown: handleKeyDown, "aria-haspopup": "listbox", "aria-expanded": isOpen, "aria-invalid": !!error, tabIndex: disabled ? -1 : 0, role: "combobox" },
8229
8296
  React.createElement("span", { className: "label" }, displayText),
8230
8297
  React.createElement(FiChevronDown, { className: "arrow" })),
8231
- isOpen && (React.createElement("div", { className: `dropdown-options scrollbar ${disabled ? 'disabled' : ''}`, ref: dropdownOptionsRef, role: "listbox" }, options.map((option, index) => (React.createElement("div", { key: `${option.value}-${index}`, className: `option ${value === option.value ? 'selected' : ''}`, onClick: () => !disabled && handleOptionClick(option.value), role: "option", "aria-selected": value === option.value }, option.label)))))),
8298
+ isOpen && (React.createElement("div", { className: "dropdown-options scrollbar", ref: dropdownOptionsRef, role: "listbox" }, options.map((option, index) => (React.createElement("div", { key: `${option.value}-${index}`, className: `option ${String(value) === String(option.value) ? 'selected' : ''} ${highlightIndex === index ? 'highlight' : ''}`, onClick: () => !disabled && handleOptionSelect(option.value), role: "option", "aria-selected": String(value) === String(option.value), tabIndex: 0 }, option.label)))))),
8232
8299
  label && floatingLabel && (React.createElement("label", { htmlFor: name },
8233
8300
  label,
8234
8301
  " ",
8235
8302
  required && React.createElement("span", { className: "text-danger" }, "*"))),
8236
- error ? (React.createElement("small", { className: "form-message text-danger" }, error)) : (message && React.createElement("small", { className: "form-message text-muted" }, message))));
8303
+ error ? (React.createElement("small", { className: "form-message text-danger", role: "alert" }, error)) : (message && React.createElement("small", { className: "form-message text-muted" }, message))));
8237
8304
  });
8238
8305
  Select.displayName = 'Select';
8239
8306