@kouji-ui/core 0.9.1 → 0.9.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.
@@ -1974,6 +1974,21 @@ function longestRunMs(durations, delays) {
1974
1974
  }
1975
1975
  /** The strategy slots of a bundle, in attach order. */
1976
1976
  const STRATEGY_SLOTS = ['mount', 'position', 'backdrop', 'scrollLock', 'focusTrap', 'liveAnnouncer', 'trigger'];
1977
+ /**
1978
+ * The controller each attached strategy instance serves. A strategy is
1979
+ * stateful (it holds one overlay's context and its inert / scroll-lock
1980
+ * release handles), so one instance must never run two overlays at once.
1981
+ */
1982
+ const strategyOwners = new WeakMap();
1983
+ /**
1984
+ * @internal The controller `strategy` is attached to, if any. `KjOverlayPanel`
1985
+ * uses it to drop strategies it inherited from an enclosing overlay (a
1986
+ * select inside a modal dialog must not run the dialog's scrim, inert and
1987
+ * focus trap as its own).
1988
+ */
1989
+ function overlayStrategyOwner(strategy) {
1990
+ return strategyOwners.get(strategy);
1991
+ }
1977
1992
  /**
1978
1993
  * Owns the overlay's open/closed state machine plus the rAF/transition
1979
1994
  * orchestration. Strategies (mount/position/backdrop/focus-trap/scroll-lock/
@@ -2125,13 +2140,21 @@ class KjOverlayController {
2125
2140
  }
2126
2141
  for (const slot of STRATEGY_SLOTS) {
2127
2142
  const old = prev[slot];
2128
- if (old && old !== next[slot])
2143
+ if (old && old !== next[slot]) {
2129
2144
  old.detach();
2145
+ if (strategyOwners.get(old) === this)
2146
+ strategyOwners.delete(old);
2147
+ }
2130
2148
  }
2131
2149
  }
2132
2150
  this.strategies = next;
2133
- for (const slot of STRATEGY_SLOTS)
2134
- next[slot]?.attach(this.context);
2151
+ for (const slot of STRATEGY_SLOTS) {
2152
+ const strategy = next[slot];
2153
+ if (!strategy)
2154
+ continue;
2155
+ strategyOwners.set(strategy, this);
2156
+ strategy.attach(this.context);
2157
+ }
2135
2158
  }
2136
2159
  open() {
2137
2160
  const cur = this._state();
@@ -2198,7 +2221,12 @@ class KjOverlayController {
2198
2221
  return;
2199
2222
  const s = this.strategies;
2200
2223
  for (const slot of [...STRATEGY_SLOTS].reverse()) {
2201
- s[slot]?.detach();
2224
+ const strategy = s[slot];
2225
+ if (!strategy)
2226
+ continue;
2227
+ strategy.detach();
2228
+ if (strategyOwners.get(strategy) === this)
2229
+ strategyOwners.delete(strategy);
2202
2230
  }
2203
2231
  this.strategies = null;
2204
2232
  }
@@ -2911,6 +2939,36 @@ function resolveTokens(self) {
2911
2939
  role: inject(KJ_OVERLAY_PANEL_ROLE, opts),
2912
2940
  };
2913
2941
  }
2942
+ /**
2943
+ * `tokens` minus every strategy already attached to a controller other than
2944
+ * `controller`. Strategies are resolved through the injector chain, so an
2945
+ * overlay rendered inside another one (a select, combobox or date picker in
2946
+ * a service-launched dialog) would otherwise find the enclosing overlay's
2947
+ * own instances for every slot it does not provide itself — and run the
2948
+ * dialog's scrim, `inert` and focus trap as if the dropdown were a second
2949
+ * modal, freezing the dialog it belongs to. Those instances belong to the
2950
+ * enclosing overlay; the nested one falls back to "none" for that slot.
2951
+ */
2952
+ function withoutForeign(tokens, controller) {
2953
+ if (!controller)
2954
+ return tokens;
2955
+ const mine = (s) => {
2956
+ if (!s)
2957
+ return s;
2958
+ const owner = overlayStrategyOwner(s);
2959
+ return owner && owner !== controller ? null : s;
2960
+ };
2961
+ return {
2962
+ ...tokens,
2963
+ mount: mine(tokens.mount),
2964
+ position: mine(tokens.position),
2965
+ backdrop: mine(tokens.backdrop),
2966
+ focusTrap: mine(tokens.focusTrap),
2967
+ scrollLock: mine(tokens.scrollLock),
2968
+ liveAnnouncer: mine(tokens.liveAnnouncer),
2969
+ trigger: mine(tokens.trigger),
2970
+ };
2971
+ }
2914
2972
  /**
2915
2973
  * Marks an element as the overlay's panel — wires it to the controller,
2916
2974
  * resolves the panel role/aria-modal, and binds DOM state attributes
@@ -2925,7 +2983,10 @@ function resolveTokens(self) {
2925
2983
  * must not inherit the dialog's scroll lock, scrim or `alertdialog` role.
2926
2984
  * A panel whose controller is on its host chain (a builder-launched body,
2927
2985
  * or the root pattern where one directive provides controller and
2928
- * strategies for the whole subtree) reads them from that chain as before.
2986
+ * strategies for the whole subtree) reads them from that chain, minus any
2987
+ * strategy instance an enclosing overlay is already running: a select inside
2988
+ * a service-launched dialog finds the dialog's scrim, focus trap and scroll
2989
+ * lock up the chain, and must not adopt them.
2929
2990
  *
2930
2991
  * A portalled panel whose scope provides a backdrop strategy also gets a
2931
2992
  * real `<kj-backdrop>` scrim (created next to the panel, moved into the
@@ -2980,8 +3041,11 @@ class KjOverlayPanel {
2980
3041
  return !!t && t.controller !== this.hostController;
2981
3042
  }, /* @ts-ignore */
2982
3043
  ...(ngDevMode ? [{ debugName: "scoped" }] : /* istanbul ignore next */ []));
2983
- /** The tokens that apply to this panel. */
2984
- tokens = computed(() => (this.scoped() ? this.own : this.inherited), /* @ts-ignore */
3044
+ /**
3045
+ * The tokens that apply to this panel. Inherited ones exclude strategies an
3046
+ * enclosing overlay already runs (see {@link withoutForeign}).
3047
+ */
3048
+ tokens = computed(() => this.scoped() ? this.own : withoutForeign(this.inherited, this.hostController), /* @ts-ignore */
2985
3049
  ...(ngDevMode ? [{ debugName: "tokens" }] : /* istanbul ignore next */ []));
2986
3050
  role = computed(() => this.tokens().role ?? 'dialog', /* @ts-ignore */
2987
3051
  ...(ngDevMode ? [{ debugName: "role" }] : /* istanbul ignore next */ []));