You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by ta...@apache.org on 2026/08/18 18:10:36 UTC

(myfaces) branch main updated: Faces S0ec 1507 - Enhance UIInput events with HTML5 like oninput

This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/myfaces.git


The following commit(s) were added to refs/heads/main by this push:
     new 6fa5084be Faces S0ec 1507 - Enhance UIInput events with HTML5 like oninput
6fa5084be is described below

commit 6fa5084be4f0cbd57b38e48ccc21363aa3d0720d
Author: tandraschko <ta...@apache.org>
AuthorDate: Tue Aug 18 20:10:45 2026 +0200

    Faces S0ec 1507 - Enhance UIInput events with HTML5 like oninput
---
 .../jakarta/faces/component/html/HtmlEvents.java   | 315 ++++++++++++++++++
 .../component/html/_HtmlOutcomeTargetButton.java   |   3 +-
 .../component/html/_HtmlOutcomeTargetLink.java     |   3 +-
 .../faces/component/html/_HtmlOutputLink.java      |   3 +-
 .../java/jakarta/faces/event/BehaviorEvent.java    |  22 ++
 .../core/api/shared/CommonHtmlAttributes.java      |  18 +-
 .../myfaces/core/api/shared/CommonHtmlEvents.java  |  54 +++-
 .../main/resources/META-INF/componentClass20.vm    |  26 ++
 .../myfaces/renderkit/html/HtmlLabelRenderer.java  |   2 +
 .../renderkit/html/base/HtmlBodyRendererBase.java  |  12 +-
 .../html/base/HtmlButtonRendererBase.java          |   3 +
 .../html/base/HtmlCheckboxRendererBase.java        |   8 +-
 .../renderkit/html/base/HtmlGroupRendererBase.java |  26 +-
 .../renderkit/html/base/HtmlLinkRendererBase.java  |  20 +-
 .../base/HtmlOutcomeTargetButtonRendererBase.java  |   4 +
 .../renderkit/html/base/HtmlRadioRendererBase.java |   4 +-
 .../myfaces/renderkit/html/base/HtmlRenderer.java  |  17 +
 .../html/base/HtmlSelectableRendererBase.java      |   3 +
 .../renderkit/html/util/CommonHtmlEventsUtil.java  | 242 ++++++++++++++
 .../facelets/component/JsfElementRenderer.java     |   2 +
 .../view/facelets/tag/faces/core/AjaxHandler.java  |   7 +-
 .../main/resources/META-INF/componentClass20.vm    |  10 +
 .../renderkit/html/HtmlFormRendererTest.java       |   2 +-
 .../renderkit/html/HtmlGridRendererTest.java       |   2 +-
 .../renderkit/html/HtmlGroupRendererTest.java      |   2 +-
 .../renderkit/html/HtmlImageRendererTest.java      |   2 +-
 .../renderkit/html/HtmlTableRendererTest.java      |   2 +-
 ...tionalHtmlEventsClientBehaviorRendererTest.java | 352 +++++++++++++++++++++
 .../HtmlBodyClientBehaviorRendererTest.java        |  70 +++-
 .../test/utils/HtmlCheckAttributesUtil.java        |  28 +-
 .../faces/html/AdditionalHtmlEventsTestCase.java   | 108 +++++++
 .../tag/faces/html/additionalHtmlEvents.xml        |  36 +++
 32 files changed, 1375 insertions(+), 33 deletions(-)

diff --git a/api/src/main/java/jakarta/faces/component/html/HtmlEvents.java b/api/src/main/java/jakarta/faces/component/html/HtmlEvents.java
new file mode 100644
index 000000000..b1a47cb47
--- /dev/null
+++ b/api/src/main/java/jakarta/faces/component/html/HtmlEvents.java
@@ -0,0 +1,315 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package jakarta.faces.component.html;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Supplier;
+
+import jakarta.faces.component.ActionSource;
+import jakarta.faces.component.EditableValueHolder;
+import jakarta.faces.component.behavior.ClientBehaviorHolder;
+import jakarta.faces.context.FacesContext;
+import jakarta.faces.event.BehaviorEvent.FacesComponentEvent;
+
+/**
+ * <p class="changed_added_5_0">
+ * Events supported by HTML elements as per
+ * <a href="https://html.spec.whatwg.org/multipage/webappapis.html#event-handlers-on-elements,-document-objects,-and-window-objects">current spec</a>.
+ * These can be used to supply {@link ClientBehaviorHolder#getEventNames()} and
+ * {@link ClientBehaviorHolder#getDefaultEventName()}.
+ * </p>
+ * <p>
+ * The events are split into two enums: {@link HtmlDocumentElementEvent} lists the events that fire on the element
+ * itself, while {@link HtmlBodyElementEvent} lists the events that are also part of the HTML spec's
+ * <code>GlobalEventHandlers</code> but have special behavior on the HTML <code>&lt;body&gt;</code> element where they
+ * are forwarded to the <code>Window</code> object instead of firing on the element. Components that represent the HTML
+ * <code>&lt;body&gt;</code> element should use {@link #getHtmlDocumentElementEventNames(FacesContext)} to avoid
+ * exposing these window-level events as component behavior events, while all other components should use
+ * {@link #getHtmlBodyElementEventNames(FacesContext)} or one of its supersets.
+ * </p>
+ *
+ * @since 5.0
+ */
+public final class HtmlEvents
+{
+
+    /**
+     * Events supported by all HTML document elements that always fire on the element itself.
+     * See {@link HtmlBodyElementEvent} for events that are forwarded to the <code>Window</code> object on the HTML
+     * <code>&lt;body&gt;</code> element.
+     */
+    public enum HtmlDocumentElementEvent
+    {
+        abort,
+        auxclick,
+        beforeinput,
+        beforematch,
+        beforetoggle,
+        cancel,
+        canplay,
+        canplaythrough,
+        change,
+        click,
+        close,
+        command,
+        contextlost,
+        contextmenu,
+        contextrestored,
+        copy,
+        cuechange,
+        cut,
+        dblclick,
+        drag,
+        dragend,
+        dragenter,
+        dragleave,
+        dragover,
+        dragstart,
+        drop,
+        durationchange,
+        emptied,
+        ended,
+        formdata,
+        input,
+        invalid,
+        keydown,
+        keypress,
+        keyup,
+        loadeddata,
+        loadedmetadata,
+        loadstart,
+        mousedown,
+        mouseenter,
+        mouseleave,
+        mousemove,
+        mouseout,
+        mouseover,
+        mouseup,
+        paste,
+        pause,
+        play,
+        playing,
+        progress,
+        ratechange,
+        reset,
+        securitypolicyviolation,
+        seeked,
+        seeking,
+        select,
+        slotchange,
+        stalled,
+        submit,
+        suspend,
+        timeupdate,
+        toggle,
+        volumechange,
+        waiting,
+        wheel;
+    }
+
+    /**
+     * Events from the HTML spec's <code>GlobalEventHandlers</code> that have special behavior on the HTML body element:
+     * instead of firing on the element itself, they are forwarded to the <code>Window</code> object.
+     * These events are valid on all HTML elements, but are listed separately here because of this distinction.
+     */
+    public enum HtmlBodyElementEvent
+    {
+        blur,
+        error,
+        focus,
+        load,
+        resize,
+        scroll,
+        scrollend;
+    }
+
+    /**
+     * The name of the context-param whose value must represent a space-separated list of additional HTML event names.
+     * All supported HTML event names are defined in the enums {@link HtmlDocumentElementEvent} and
+     * {@link HtmlBodyElementEvent}. Any HTML event name which you wish to add to these enums can be supplied via this
+     * context-param. Duplicates will be automatically filtered, case sensitive.
+     */
+    public static final String ADDITIONAL_HTML_EVENT_NAMES_PARAM_NAME = "jakarta.faces.ADDITIONAL_HTML_EVENT_NAMES";
+
+    private enum CacheKey
+    {
+        ADDITIONAL_HTML_EVENT_NAMES,
+        HTML_DOCUMENT_ELEMENT_EVENT_NAMES,
+        HTML_BODY_ELEMENT_EVENT_NAMES,
+        FACES_ACTION_SOURCE_EVENT_NAMES,
+        FACES_EDITABLE_VALUE_HOLDER_EVENT_NAMES;
+    }
+
+    private HtmlEvents()
+    {
+        throw new AssertionError();
+    }
+
+    /**
+     * @param context The involved faces context.
+     * @return All additional HTML event names specified via {@link #ADDITIONAL_HTML_EVENT_NAMES_PARAM_NAME}.
+     */
+    public static Collection<String> getAdditionalHtmlEventNames(FacesContext context)
+    {
+        return cache(context, CacheKey.ADDITIONAL_HTML_EVENT_NAMES, () -> getContextParam(context));
+    }
+
+    /**
+     * @param context The involved faces context.
+     * @return All supported event names for HTML document elements, including additional HTML event names.
+     */
+    public static Collection<String> getHtmlDocumentElementEventNames(FacesContext context)
+    {
+        return cache(context, CacheKey.HTML_DOCUMENT_ELEMENT_EVENT_NAMES,
+                () -> merge(getAdditionalHtmlEventNames(context), HtmlDocumentElementEvent.values()));
+    }
+
+    /**
+     * @param context The involved faces context.
+     * @return All supported event names for HTML body elements, including HTML document element event names.
+     */
+    public static Collection<String> getHtmlBodyElementEventNames(FacesContext context)
+    {
+        return cache(context, CacheKey.HTML_BODY_ELEMENT_EVENT_NAMES,
+                () -> merge(getHtmlDocumentElementEventNames(context), HtmlBodyElementEvent.values()));
+    }
+
+    /**
+     * @param context The involved faces context.
+     * @return All supported event names for HTML implementations of Faces {@link ActionSource} components, including
+     *         HTML body element event names.
+     */
+    public static Collection<String> getFacesActionSourceEventNames(FacesContext context)
+    {
+        return cache(context, CacheKey.FACES_ACTION_SOURCE_EVENT_NAMES,
+                () -> merge(getHtmlBodyElementEventNames(context), FacesComponentEvent.action));
+    }
+
+    /**
+     * @param context The involved faces context.
+     * @return All supported event names for HTML implementations of Faces {@link EditableValueHolder} components,
+     *         including HTML body element event names.
+     */
+    public static Collection<String> getFacesEditableValueHolderEventNames(FacesContext context)
+    {
+        return cache(context, CacheKey.FACES_EDITABLE_VALUE_HOLDER_EVENT_NAMES,
+                () -> merge(getHtmlBodyElementEventNames(context), FacesComponentEvent.valueChange));
+    }
+
+    private static Collection<String> getContextParam(FacesContext context)
+    {
+        String param = context.getExternalContext().getInitParameter(ADDITIONAL_HTML_EVENT_NAMES_PARAM_NAME);
+        if (param == null || param.isBlank())
+        {
+            return Collections.emptyList();
+        }
+
+        List<String> eventNames = new ArrayList<>();
+        for (String eventName : param.trim().split("\\s+"))
+        {
+            if (!eventName.isEmpty())
+            {
+                eventNames.add(eventName);
+            }
+        }
+        return Collections.unmodifiableList(eventNames);
+    }
+
+    private static Collection<String> merge(Collection<String> eventNames, Enum<?>... enumValues)
+    {
+        // a TreeSet gives us the sorted + distinct semantics in one go.
+        TreeSet<String> merged = new TreeSet<>(eventNames);
+        for (Enum<?> enumValue : enumValues)
+        {
+            merged.add(enumValue.name());
+        }
+        return new EventNameCollection(merged);
+    }
+
+    /**
+     * Sorted, distinct and unmodifiable like the plain <code>List</code> the spec javadoc suggests, but with
+     * constant time {@link #contains}, which is what {@link jakarta.faces.component.UIComponentBase#addClientBehavior}
+     * and the renderers actually do with this collection.
+     */
+    private static final class EventNameCollection extends java.util.AbstractList<String>
+            implements java.util.RandomAccess
+    {
+        private final String[] sortedEventNames;
+        private final Set<String> lookup;
+
+        private EventNameCollection(TreeSet<String> sortedDistinctEventNames)
+        {
+            this.sortedEventNames = sortedDistinctEventNames.toArray(new String[sortedDistinctEventNames.size()]);
+            this.lookup = new HashSet<>(sortedDistinctEventNames);
+        }
+
+        @Override
+        public String get(int index)
+        {
+            return sortedEventNames[index];
+        }
+
+        @Override
+        public int size()
+        {
+            return sortedEventNames.length;
+        }
+
+        @Override
+        public boolean contains(Object eventName)
+        {
+            return lookup.contains(eventName);
+        }
+    }
+
+    @SuppressWarnings("unchecked") // the event-name cache is stored under an Object-valued application-map entry
+    private static Collection<String> cache(FacesContext context, CacheKey key, Supplier<Collection<String>> supplier)
+    {
+        Map<String, Object> applicationMap = context.getExternalContext().getApplicationMap();
+
+        // NOTE: this may concurrently create more than one cache map, which is harmless as all of them would hold
+        // equal values anyway, and the map itself is concurrent so no entry can ever get lost or corrupted.
+        Map<CacheKey, Collection<String>> cache =
+                (Map<CacheKey, Collection<String>>) applicationMap.get(ADDITIONAL_HTML_EVENT_NAMES_PARAM_NAME);
+        if (cache == null)
+        {
+            cache = new ConcurrentHashMap<>(CacheKey.values().length);
+            applicationMap.put(ADDITIONAL_HTML_EVENT_NAMES_PARAM_NAME, cache);
+        }
+
+        // NOTE: deliberately no computeIfAbsent, the suppliers build on each other and hence recursively populate
+        // this very map, which a ConcurrentHashMap rejects with "Recursive update". A plain get/put is fine here:
+        // the values are immutable and only ever computed from the same input, so a race just recomputes them.
+        Collection<String> eventNames = cache.get(key);
+        if (eventNames == null)
+        {
+            eventNames = supplier.get();
+            cache.put(key, eventNames);
+        }
+        return eventNames;
+    }
+}
diff --git a/api/src/main/java/jakarta/faces/component/html/_HtmlOutcomeTargetButton.java b/api/src/main/java/jakarta/faces/component/html/_HtmlOutcomeTargetButton.java
index a9e9d5894..2093f8073 100644
--- a/api/src/main/java/jakarta/faces/component/html/_HtmlOutcomeTargetButton.java
+++ b/api/src/main/java/jakarta/faces/component/html/_HtmlOutcomeTargetButton.java
@@ -30,7 +30,8 @@
 @JSFComponent(name="h:button",
         clazz = "jakarta.faces.component.html.HtmlOutcomeTargetButton",template=true,
         implementz = "jakarta.faces.component.behavior.ClientBehaviorHolder",
-        defaultRendererType = "jakarta.faces.Button")
+        defaultRendererType = "jakarta.faces.Button",
+        defaultEventName = "click")
 abstract class _HtmlOutcomeTargetButton extends UIOutcomeTarget implements _FocusBlurProperties,
 _EventProperties, _StyleProperties, _UniversalProperties, _AccesskeyProperty,
 _TabindexProperty, _AltProperty, _RoleProperty
diff --git a/api/src/main/java/jakarta/faces/component/html/_HtmlOutcomeTargetLink.java b/api/src/main/java/jakarta/faces/component/html/_HtmlOutcomeTargetLink.java
index c874c1cf2..fd3694939 100644
--- a/api/src/main/java/jakarta/faces/component/html/_HtmlOutcomeTargetLink.java
+++ b/api/src/main/java/jakarta/faces/component/html/_HtmlOutcomeTargetLink.java
@@ -29,7 +29,8 @@
 @JSFComponent(name="h:link",
         clazz = "jakarta.faces.component.html.HtmlOutcomeTargetLink",template=true,
         implementz = "jakarta.faces.component.behavior.ClientBehaviorHolder",
-        defaultRendererType = "jakarta.faces.Link")
+        defaultRendererType = "jakarta.faces.Link",
+        defaultEventName = "click")
 abstract class _HtmlOutcomeTargetLink extends UIOutcomeTarget implements _FocusBlurProperties,
 _EventProperties, _StyleProperties, _UniversalProperties, _AccesskeyProperty,
 _TabindexProperty, _LinkProperties, _RoleProperty
diff --git a/api/src/main/java/jakarta/faces/component/html/_HtmlOutputLink.java b/api/src/main/java/jakarta/faces/component/html/_HtmlOutputLink.java
index d6dc1eaff..e93ef6124 100644
--- a/api/src/main/java/jakarta/faces/component/html/_HtmlOutputLink.java
+++ b/api/src/main/java/jakarta/faces/component/html/_HtmlOutputLink.java
@@ -33,7 +33,8 @@
         clazz = "jakarta.faces.component.html.HtmlOutputLink",template=true,
         tagClass = "org.apache.myfaces.taglib.html.HtmlOutputLinkTag",
         defaultRendererType = "jakarta.faces.Link",
-        implementz = "jakarta.faces.component.behavior.ClientBehaviorHolder")
+        implementz = "jakarta.faces.component.behavior.ClientBehaviorHolder",
+        defaultEventName = "click")
 abstract class _HtmlOutputLink extends UIOutput implements _AccesskeyProperty,
 _UniversalProperties, _FocusBlurProperties, _EventProperties, _StyleProperties,
 _TabindexProperty, _LinkProperties, _RoleProperty
diff --git a/api/src/main/java/jakarta/faces/event/BehaviorEvent.java b/api/src/main/java/jakarta/faces/event/BehaviorEvent.java
index d163e5f49..a3fe45bc7 100644
--- a/api/src/main/java/jakarta/faces/event/BehaviorEvent.java
+++ b/api/src/main/java/jakarta/faces/event/BehaviorEvent.java
@@ -18,6 +18,8 @@
  */
 package jakarta.faces.event;
 
+import jakarta.faces.component.ActionSource;
+import jakarta.faces.component.EditableValueHolder;
 import jakarta.faces.component.UIComponent;
 import jakarta.faces.component.behavior.Behavior;
 import jakarta.faces.context.FacesContext;
@@ -27,6 +29,26 @@
  */
 public abstract class BehaviorEvent extends FacesEvent
 {
+    /**
+     * <p class="changed_added_5_0">
+     * Behavior events supported by Faces components.
+     * </p>
+     *
+     * @since 5.0
+     */
+    public enum FacesComponentEvent
+    {
+        /**
+         * Default event of {@link ActionSource} components.
+         */
+        action,
+
+        /**
+         * Default event of {@link EditableValueHolder} components.
+         */
+        valueChange;
+    }
+
     private Behavior _behavior;
 
     public BehaviorEvent(FacesContext facesContext, UIComponent uiComponent, Behavior behavior)
diff --git a/api/src/main/java/org/apache/myfaces/core/api/shared/CommonHtmlAttributes.java b/api/src/main/java/org/apache/myfaces/core/api/shared/CommonHtmlAttributes.java
index 95d83738d..0ff94741d 100644
--- a/api/src/main/java/org/apache/myfaces/core/api/shared/CommonHtmlAttributes.java
+++ b/api/src/main/java/org/apache/myfaces/core/api/shared/CommonHtmlAttributes.java
@@ -223,19 +223,23 @@ public class CommonHtmlAttributes
 
     public static void markAttribute(UIComponent component, String name)
     {
+        // A ValueExpression bound to a behavior event attribute, e.g. oninput="#{bean.script}", is neither a
+        // component property nor part of the component attribute map key set, so it would be invisible at render
+        // time when the renderer has no matching renderer specific attribute. Remember its name here.
+        if (CommonHtmlEvents.isBehaviorEventAttribute(name))
+        {
+            CommonHtmlEvents.markEventAttribute(component, name);
+        }
+
         Long propertyConstant = COMMONERTIES_KEY_BY_NAME.get(name);
         if (propertyConstant == null)
         {
             return;
         }
-        Long commonPropertiesSet = (Long) component.getAttributes().get(ATTRIBUTES_MARKED);
-        if (commonPropertiesSet == null)
-        {
-            commonPropertiesSet = 0L;
-        }
-        component.getAttributes().put(ATTRIBUTES_MARKED, commonPropertiesSet | propertyConstant);
+
+        markAttribute(component, propertyConstant);
     }
-    
+
     public static void markAttribute(UIComponent component, long propertyConstant)
     {
         Long commonPropertiesSet = (Long) component.getAttributes().get(ATTRIBUTES_MARKED);
diff --git a/api/src/main/java/org/apache/myfaces/core/api/shared/CommonHtmlEvents.java b/api/src/main/java/org/apache/myfaces/core/api/shared/CommonHtmlEvents.java
index dc33923c4..e093901be 100644
--- a/api/src/main/java/org/apache/myfaces/core/api/shared/CommonHtmlEvents.java
+++ b/api/src/main/java/org/apache/myfaces/core/api/shared/CommonHtmlEvents.java
@@ -19,13 +19,29 @@
 package org.apache.myfaces.core.api.shared;
 
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 import jakarta.faces.component.UIComponent;
 
 public class CommonHtmlEvents
 {
     public static final String EVENTS_MARKED = "oam.EVENTS_MARKED";
-    
+
+    /**
+     * Prefix of the HTML attribute which carries the script of a behavior event, e.g. "onclick" for "click".
+     */
+    public static final String BEHAVIOR_EVENT_ATTRIBUTE_PREFIX = "on";
+
+    /**
+     * Component attribute under which the names of all behavior event attributes bound to a
+     * <code>ValueExpression</code>, e.g. <code>oninput="#{bean.script}"</code>, are remembered as a
+     * <code>Set&lt;String&gt;</code>. Such an attribute is neither a component property nor part of the component
+     * attribute map key set, so this marker is the only cheap way for a renderer to find it at render time; see
+     * <code>CommonHtmlEventsUtil#renderAdditionalBehaviorEventHandlers</code>.
+     */
+    public static final String EVENT_ATTRIBUTES_MARKED = "oam.EVENT_ATTRIBUTES_MARKED";
+
     public static final long ACTION        = 0x1L;
     public static final long CLICK         = 0x2L;
     public static final long DBLCLICK      = 0x4L;
@@ -130,4 +146,40 @@ public static long getMarkedEvents(UIComponent component)
         }
         return commonEvents;
     }
+
+    /**
+     * @param name The attribute name to check.
+     * @return <code>true</code> if the given attribute name denotes a behavior event attribute, i.e. it starts with
+     *         "on" and has at least one more character.
+     */
+    public static boolean isBehaviorEventAttribute(String name)
+    {
+        return name.length() > BEHAVIOR_EVENT_ATTRIBUTE_PREFIX.length()
+                && name.charAt(0) == 'o'
+                && name.charAt(1) == 'n';
+    }
+
+    /**
+     * @param name The behavior event attribute name, as accepted by {@link #isBehaviorEventAttribute(String)}.
+     * @return The event name of the given behavior event attribute name, e.g. "input" for "oninput".
+     */
+    public static String getEventName(String name)
+    {
+        return name.substring(BEHAVIOR_EVENT_ATTRIBUTE_PREFIX.length());
+    }
+
+    /**
+     * Remembers that a <code>ValueExpression</code> has been bound to the given behavior event attribute,
+     * see {@link #EVENT_ATTRIBUTES_MARKED}. Mirrors the copy-on-write pattern of {@link #markEvent} so that
+     * partial state saving picks up the change.
+     */
+    @SuppressWarnings("unchecked") // the marker entry is stored under an Object-valued component attribute
+    public static void markEventAttribute(UIComponent component, String name)
+    {
+        Map<String, Object> attributes = component.getAttributes();
+        Set<String> marked = (Set<String>) attributes.get(EVENT_ATTRIBUTES_MARKED);
+        Set<String> newMarked = marked == null ? new HashSet<>(4) : new HashSet<>(marked);
+        newMarked.add(name);
+        attributes.put(EVENT_ATTRIBUTES_MARKED, newMarked);
+    }
 }
diff --git a/api/src/main/resources/META-INF/componentClass20.vm b/api/src/main/resources/META-INF/componentClass20.vm
index 67837c442..82a45a7ed 100644
--- a/api/src/main/resources/META-INF/componentClass20.vm
+++ b/api/src/main/resources/META-INF/componentClass20.vm
@@ -105,6 +105,31 @@ $innersource
 #if ($component.implements)
 #if ( ($component.implements == "jakarta.faces.component.behavior.ClientBehaviorHolder")
      && (${utils.getClassFromFullClass($component.className)} != "UIWebsocket"))
+## Spec issue 1507: the standard HTML components must expose every HTML event name, so that both
+## <f:ajax event="input"> and a plain oninput="..." attribute work without the component having to
+## declare a matching property. See jakarta.faces.component.html.HtmlEvents.
+#if ($component.className.startsWith("jakarta.faces.component.html"))
+#if (${utils.getClassFromFullClass($component.className)} == "HtmlBody")
+## the <body> element forwards blur/error/focus/load/resize/scroll/scrollend to the Window object,
+## hence they must not be exposed as behavior events of the component itself.
+#set ($htmlEventNamesMethod = "getHtmlDocumentElementEventNames")
+#else
+#if ($component.defaultEventName == "action")
+#set ($htmlEventNamesMethod = "getFacesActionSourceEventNames")
+#else
+#if ($component.defaultEventName == "valueChange")
+#set ($htmlEventNamesMethod = "getFacesEditableValueHolderEventNames")
+#else
+#set ($htmlEventNamesMethod = "getHtmlBodyElementEventNames")
+#end
+#end
+#end
+    @Override
+    public java.util.Collection<String> getEventNames()
+    {
+        return jakarta.faces.component.html.HtmlEvents.${htmlEventNamesMethod}(getFacesContext());
+    }
+#else
     static private final java.util.Collection<String> CLIENT_EVENTS_LIST =
         java.util.Collections.unmodifiableCollection(
             java.util.Arrays.asList(
@@ -123,6 +148,7 @@ $innersource
     {
         return CLIENT_EVENTS_LIST;
     }
+#end
 
     @Override
     public void addClientBehavior(String eventName, jakarta.faces.component.behavior.ClientBehavior behavior)
diff --git a/impl/src/main/java/org/apache/myfaces/renderkit/html/HtmlLabelRenderer.java b/impl/src/main/java/org/apache/myfaces/renderkit/html/HtmlLabelRenderer.java
index 8cc44f25f..96d5c0b78 100755
--- a/impl/src/main/java/org/apache/myfaces/renderkit/html/HtmlLabelRenderer.java
+++ b/impl/src/main/java/org/apache/myfaces/renderkit/html/HtmlLabelRenderer.java
@@ -136,6 +136,8 @@ public void encodeBegin(FacesContext facesContext, UIComponent uiComponent) thro
                             writer, uiComponent, behaviors);
                 }
             }
+            CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, uiComponent, behaviors,
+                    CommonHtmlEventsUtil.RENDERER_HANDLED_FOCUS_BLUR_EVENTS);
             if (isCommonPropertiesOptimizationEnabled(facesContext))
             {
                 CommonHtmlAttributesUtil.renderLabelPassthroughPropertiesWithoutEvents(writer, 
diff --git a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlBodyRendererBase.java b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlBodyRendererBase.java
index 976704ca3..fb30e29c9 100644
--- a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlBodyRendererBase.java
+++ b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlBodyRendererBase.java
@@ -25,6 +25,7 @@
 import java.io.IOException;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import jakarta.faces.application.ProjectStage;
 import jakarta.faces.component.UIComponent;
@@ -50,10 +51,19 @@ public void decode(FacesContext context, T component)
     {
         // check for npe
         super.decode(context, component);
-        
+
         ClientBehaviorRendererUtils.decodeClientBehaviors(context, component);
     }
 
+    /**
+     * On top of the common events, this renderer takes care of onload/onunload itself, see encodeBegin.
+     */
+    @Override
+    protected Set<String> getRendererHandledEvents()
+    {
+        return CommonHtmlEventsUtil.RENDERER_HANDLED_BODY_EVENTS;
+    }
+
     @Override
     public void encodeBegin(FacesContext facesContext, T component) throws IOException
     {
diff --git a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlButtonRendererBase.java b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlButtonRendererBase.java
index 01050e31b..85d7ee228 100644
--- a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlButtonRendererBase.java
+++ b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlButtonRendererBase.java
@@ -272,6 +272,9 @@ public void encodeBegin(FacesContext facesContext, T uiComponent) throws IOExcep
             }
         }
 
+        CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, uiComponent, behaviors,
+                CommonHtmlEventsUtil.RENDERER_HANDLED_COMMAND_BUTTON_EVENTS);
+
         if (isDisabled(facesContext, uiComponent))
         {
             writer.writeAttribute(HTML.DISABLED_ATTR, HTML.DISABLED_ATTR, ComponentAttrs.DISABLED_ATTR);
diff --git a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlCheckboxRendererBase.java b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlCheckboxRendererBase.java
index c14d1a54f..2130b2e8a 100644
--- a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlCheckboxRendererBase.java
+++ b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlCheckboxRendererBase.java
@@ -428,6 +428,9 @@ else if (renderId)
                                 facesContext, writer, uiComponent, itemId != null ? itemId : clientId, behaviors);
                     }
                 }
+                CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(
+                        facesContext, writer, uiComponent, behaviors,
+                        CommonHtmlEventsUtil.RENDERER_HANDLED_FIELD_EVENTS);
                 HtmlRendererUtils.renderHTMLAttributes(writer, uiComponent,
                         HTML.INPUT_PASSTHROUGH_ATTRIBUTES_WITHOUT_DISABLED_AND_EVENTS);
             }
@@ -473,7 +476,10 @@ else if (renderId)
                                 facesContext, writer, uiComponent, itemId != null ? itemId : clientId, behaviors);
                     }
                 }
-                HtmlRendererUtils.renderHTMLAttributes(writer, uiComponent, 
+                CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(
+                        facesContext, writer, uiComponent, behaviors,
+                        CommonHtmlEventsUtil.RENDERER_HANDLED_FIELD_EVENTS);
+                HtmlRendererUtils.renderHTMLAttributes(writer, uiComponent,
                         HTML.INPUT_PASSTHROUGH_ATTRIBUTES_WITHOUT_DISABLED_AND_STYLE_AND_EVENTS);
             }
             else
diff --git a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlGroupRendererBase.java b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlGroupRendererBase.java
index 44984786b..0c72279a6 100644
--- a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlGroupRendererBase.java
+++ b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlGroupRendererBase.java
@@ -96,6 +96,11 @@ private boolean needsWrapper(FacesContext context, UIComponent component)
         {
             return true;
         }
+        if (CommonHtmlEventsUtil.hasAdditionalBehaviorEventHandlers(component, behaviors,
+                CommonHtmlEventsUtil.RENDERER_HANDLED_COMMON_EVENTS))
+        {
+            return true;
+        }
         Long commonPropertiesMarked = getCommonPropertiesMarked(context, component);
         if (commonPropertiesMarked != null)
         {
@@ -169,21 +174,40 @@ private void writeWrapperStart(FacesContext context, UIComponent component) thro
                     HtmlRendererUtils.renderBehaviorizedEventHandlers(context, writer, component, behaviors);
                 }
             }
+
+            CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(context, writer, component, behaviors,
+                    CommonHtmlEventsUtil.RENDERER_HANDLED_COMMON_EVENTS);
         }
         else
         {
+            boolean elementStarted = false;
             if (commonPropertiesMarked != null && commonPropertiesMarked > 0)
             {
                 writer.startElement(layoutElement, component);
                 HtmlRendererUtils.writeIdIfNecessary(writer, component, context);
                 CommonHtmlAttributesUtil.renderCommonPassthroughProperties(
                         writer, commonPropertiesMarked, component);
+                elementStarted = true;
             }
             else if (commonPropertiesMarked == null)
             {
-                HtmlRendererUtils.renderHTMLAttributesWithOptionalStartElement(writer,
+                elementStarted = HtmlRendererUtils.renderHTMLAttributesWithOptionalStartElement(writer,
                         component, layoutElement, HTML.COMMON_PASSTROUGH_ATTRIBUTES);
             }
+
+            // behavior event attributes without matching property, e.g. oninput="...";
+            // when present, needsWrapper() has already forced the wrapper element, so it must be started here
+            if (CommonHtmlEventsUtil.hasAdditionalBehaviorEventHandlers(component, behaviors,
+                    CommonHtmlEventsUtil.RENDERER_HANDLED_COMMON_EVENTS))
+            {
+                if (!elementStarted)
+                {
+                    writer.startElement(layoutElement, component);
+                    HtmlRendererUtils.writeIdIfNecessary(writer, component, context);
+                }
+                CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(context, writer, component, behaviors,
+                        CommonHtmlEventsUtil.RENDERER_HANDLED_COMMON_EVENTS);
+            }
         }
     }
 
diff --git a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlLinkRendererBase.java b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlLinkRendererBase.java
index 9c7679799..54836c3a7 100644
--- a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlLinkRendererBase.java
+++ b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlLinkRendererBase.java
@@ -428,6 +428,9 @@ protected void renderCommandLinkStart(FacesContext facesContext, UIComponent com
             HtmlRendererUtils.renderHTMLAttribute(writer, HTML.STYLE_CLASS_ATTR, HTML.STYLE_CLASS_ATTR, styleClass);
         }
 
+        CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, component, behaviors,
+                CommonHtmlEventsUtil.RENDERER_HANDLED_LINK_EVENTS);
+
         // render value as required by Faces 1.1 renderkitdocs
         if(value != null)
         {
@@ -794,9 +797,12 @@ protected void renderOutputLinkStart(FacesContext facesContext, UIOutput output)
                     HtmlRendererUtils.renderHTMLAttributes(writer, output, HTML.ANCHOR_PASSTHROUGH_ATTRIBUTES_DISABLED);
                 }
             }
+
+            CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, output, behaviors,
+                    CommonHtmlEventsUtil.RENDERER_HANDLED_LINK_EVENTS);
         }
         else
-        { 
+        {
             //calculate href
             String href = RendererUtils.getStringValue(facesContext, output);
             
@@ -902,6 +908,10 @@ else if (isAnchorInHref)
                     HtmlRendererUtils.renderHTMLAttributes(writer, output, HTML.ANCHOR_PASSTHROUGH_ATTRIBUTES);
                 }
             }
+
+            CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, output, behaviors,
+                    CommonHtmlEventsUtil.RENDERER_HANDLED_LINK_EVENTS);
+
             writer.flush();
         }
     }
@@ -984,6 +994,9 @@ protected void renderOutcomeLinkStart(FacesContext facesContext, UIOutcomeTarget
                 }
             }
 
+            CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, output, behaviors,
+                    CommonHtmlEventsUtil.RENDERER_HANDLED_LINK_EVENTS);
+
             Object value = output.getValue();
 
             if(value != null)
@@ -1054,11 +1067,14 @@ protected void renderOutcomeLinkStart(FacesContext facesContext, UIOutcomeTarget
                 }
                 else
                 {
-                    HtmlRendererUtils.renderHTMLAttributes(writer, output, 
+                    HtmlRendererUtils.renderHTMLAttributes(writer, output,
                             HTML.ANCHOR_PASSTHROUGH_ATTRIBUTES);
                 }
             }
 
+            CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, output, behaviors,
+                    CommonHtmlEventsUtil.RENDERER_HANDLED_LINK_EVENTS);
+
             writer.flush();
         }
     }
diff --git a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlOutcomeTargetButtonRendererBase.java b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlOutcomeTargetButtonRendererBase.java
index 1499f3006..1add4a6cf 100644
--- a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlOutcomeTargetButtonRendererBase.java
+++ b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlOutcomeTargetButtonRendererBase.java
@@ -20,6 +20,7 @@
 
 import org.apache.myfaces.renderkit.html.util.HtmlRendererUtils;
 import org.apache.myfaces.renderkit.html.util.CommonHtmlAttributesUtil;
+import org.apache.myfaces.renderkit.html.util.CommonHtmlEventsUtil;
 import java.io.IOException;
 import java.util.List;
 import java.util.Map;
@@ -177,6 +178,9 @@ public void encodeBegin(FacesContext facesContext, T component) throws IOExcepti
                     HTML.ALT_ATTR, HTML.ALT_ATTR);
         }
 
+        CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, component, behaviors,
+                CommonHtmlEventsUtil.RENDERER_HANDLED_FOCUS_BLUR_EVENTS);
+
         writer.flush();
     }
 
diff --git a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlRadioRendererBase.java b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlRadioRendererBase.java
index c68188748..78f91f1c7 100644
--- a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlRadioRendererBase.java
+++ b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlRadioRendererBase.java
@@ -530,7 +530,9 @@ else if (renderId)
                             facesContext, writer, uiComponent, itemId != null ? itemId : clientId, behaviors);
                 }
             }
-            HtmlRendererUtils.renderHTMLAttributes(writer, uiComponent, 
+            CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, uiComponent, behaviors,
+                    CommonHtmlEventsUtil.RENDERER_HANDLED_FIELD_EVENTS);
+            HtmlRendererUtils.renderHTMLAttributes(writer, uiComponent,
                     HTML.INPUT_PASSTHROUGH_ATTRIBUTES_WITHOUT_DISABLED_AND_STYLE_AND_EVENTS);
         }
         else
diff --git a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlRenderer.java b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlRenderer.java
index 310c59638..0047415af 100644
--- a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlRenderer.java
+++ b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlRenderer.java
@@ -30,6 +30,7 @@
 import java.io.IOException;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import org.apache.myfaces.renderkit.html.util.CommonHtmlAttributesUtil;
 import org.apache.myfaces.renderkit.html.util.CommonHtmlEventsUtil;
 import org.apache.myfaces.renderkit.html.util.HTML;
@@ -205,6 +206,19 @@ protected void renderEventHandlers(FacesContext facesContext, ResponseWriter wri
                 HtmlRendererUtils.renderBehaviorizedEventHandlers(facesContext, writer, component, behaviors);
             }
         }
+
+        CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, component, behaviors,
+                getRendererHandledEvents());
+    }
+
+    /**
+     * The behavior events {@link #renderEventHandlers} takes care of itself; every other supported event
+     * is rendered generically afterwards. Renderers which render further event attributes on their own
+     * (like the body renderer with onload/onunload) must override this accordingly.
+     */
+    protected Set<String> getRendererHandledEvents()
+    {
+        return CommonHtmlEventsUtil.RENDERER_HANDLED_COMMON_EVENTS;
     }
 
     /**
@@ -246,5 +260,8 @@ protected void renderFieldEventHandlers(FacesContext facesContext, ResponseWrite
                         facesContext, writer, component, behaviors);
             }
         }
+
+        CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, component, behaviors,
+                CommonHtmlEventsUtil.RENDERER_HANDLED_FIELD_EVENTS);
     }
 }
diff --git a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlSelectableRendererBase.java b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlSelectableRendererBase.java
index 743c90ea8..1c04e7d9a 100644
--- a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlSelectableRendererBase.java
+++ b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlSelectableRendererBase.java
@@ -163,6 +163,9 @@ private void renderSelectEventHandlers(FacesContext facesContext, ResponseWriter
                         facesContext, writer, uiComponent, behaviors);
             }
         }
+
+        CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, uiComponent, behaviors,
+                CommonHtmlEventsUtil.RENDERER_HANDLED_SELECTABLE_EVENTS);
     }
 
 }
diff --git a/impl/src/main/java/org/apache/myfaces/renderkit/html/util/CommonHtmlEventsUtil.java b/impl/src/main/java/org/apache/myfaces/renderkit/html/util/CommonHtmlEventsUtil.java
index 7b8da20f4..0f5a44ac2 100644
--- a/impl/src/main/java/org/apache/myfaces/renderkit/html/util/CommonHtmlEventsUtil.java
+++ b/impl/src/main/java/org/apache/myfaces/renderkit/html/util/CommonHtmlEventsUtil.java
@@ -20,12 +20,18 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
 import jakarta.faces.component.UIComponent;
 import jakarta.faces.component.behavior.ClientBehavior;
 import jakarta.faces.component.behavior.ClientBehaviorContext;
+import jakarta.faces.component.behavior.ClientBehaviorHolder;
 import jakarta.faces.context.FacesContext;
 import jakarta.faces.context.PartialResponseWriter;
 import jakarta.faces.context.ResponseWriter;
@@ -46,6 +52,242 @@ public static long getMarkedEvents(UIComponent component)
         return CommonHtmlEvents.getMarkedEvents(component);
     }
 
+    /**
+     * The event names always rendered by the standard HTML renderers via the various
+     * <code>renderBehaviorizedEventHandlers</code> flavors: the generic mouse, key and click events.
+     * Building block for the per-renderer sets below, which name the events the respective renderer takes care of
+     * itself; every other event has to go through {@link #renderAdditionalBehaviorEventHandlers}.
+     */
+    private static final Set<String> COMMON_DOM_EVENTS = Set.of(
+            ClientBehaviorEvents.CLICK, ClientBehaviorEvents.DBLCLICK,
+            ClientBehaviorEvents.MOUSEDOWN, ClientBehaviorEvents.MOUSEUP, ClientBehaviorEvents.MOUSEOVER,
+            ClientBehaviorEvents.MOUSEMOVE, ClientBehaviorEvents.MOUSEOUT,
+            ClientBehaviorEvents.KEYPRESS, ClientBehaviorEvents.KEYDOWN, ClientBehaviorEvents.KEYUP);
+
+    /**
+     * Events handled by renderers of plain elements: div/span/table/form/img and alike.
+     * See {@link org.apache.myfaces.renderkit.html.base.HtmlRenderer#renderEventHandlers}.
+     */
+    public static final Set<String> RENDERER_HANDLED_COMMON_EVENTS = COMMON_DOM_EVENTS;
+
+    /**
+     * Events handled by renderers of input fields: change, focus, blur, select on top of the common ones,
+     * plus the virtual valueChange which is rendered on the change attribute.
+     * See {@link org.apache.myfaces.renderkit.html.base.HtmlRenderer#renderFieldEventHandlers}.
+     */
+    public static final Set<String> RENDERER_HANDLED_FIELD_EVENTS = merge(COMMON_DOM_EVENTS,
+            ClientBehaviorEvents.CHANGE, ClientBehaviorEvents.FOCUS, ClientBehaviorEvents.BLUR,
+            ClientBehaviorEvents.SELECT, ClientBehaviorEvents.VALUECHANGE);
+
+    /**
+     * Events handled by the listbox/menu renderers: like the field events, but without select
+     * (renderBehaviorizedFieldEventHandlersWithoutOnchangeAndOnselect).
+     */
+    public static final Set<String> RENDERER_HANDLED_SELECTABLE_EVENTS = merge(COMMON_DOM_EVENTS,
+            ClientBehaviorEvents.CHANGE, ClientBehaviorEvents.FOCUS, ClientBehaviorEvents.BLUR,
+            ClientBehaviorEvents.VALUECHANGE);
+
+    /**
+     * Events handled by the label/outcome target button renderers: only focus and blur on top of the common ones.
+     */
+    public static final Set<String> RENDERER_HANDLED_FOCUS_BLUR_EVENTS = merge(COMMON_DOM_EVENTS,
+            ClientBehaviorEvents.FOCUS, ClientBehaviorEvents.BLUR);
+
+    /**
+     * Events handled by the command button renderer: the field events plus the virtual action, which is rendered
+     * on the click attribute.
+     */
+    public static final Set<String> RENDERER_HANDLED_COMMAND_BUTTON_EVENTS = merge(COMMON_DOM_EVENTS,
+            ClientBehaviorEvents.CHANGE, ClientBehaviorEvents.FOCUS, ClientBehaviorEvents.BLUR,
+            ClientBehaviorEvents.SELECT, ClientBehaviorEvents.ACTION);
+
+    /**
+     * Events handled by the link renderers: focus and blur plus the virtual action, which is rendered
+     * on the click attribute.
+     */
+    public static final Set<String> RENDERER_HANDLED_LINK_EVENTS = merge(COMMON_DOM_EVENTS,
+            ClientBehaviorEvents.FOCUS, ClientBehaviorEvents.BLUR, ClientBehaviorEvents.ACTION);
+
+    /**
+     * Events handled by the body renderer: load and unload on top of the common ones.
+     */
+    public static final Set<String> RENDERER_HANDLED_BODY_EVENTS = merge(COMMON_DOM_EVENTS,
+            ClientBehaviorEvents.LOAD, ClientBehaviorEvents.UNLOAD);
+
+    /**
+     * Events handled by the jsf:element renderer: focus, blur, change, select, load and unload
+     * on top of the common ones.
+     */
+    public static final Set<String> RENDERER_HANDLED_PASSTHROUGH_ELEMENT_EVENTS = merge(COMMON_DOM_EVENTS,
+            ClientBehaviorEvents.FOCUS, ClientBehaviorEvents.BLUR, ClientBehaviorEvents.CHANGE,
+            ClientBehaviorEvents.SELECT, ClientBehaviorEvents.LOAD, ClientBehaviorEvents.UNLOAD);
+
+    private static Set<String> merge(Set<String> events, String... moreEvents)
+    {
+        Set<String> merged = new HashSet<>(events);
+        merged.addAll(Arrays.asList(moreEvents));
+        return Collections.unmodifiableSet(merged);
+    }
+
+    /**
+     * Renders every behavior event attribute which is <em>not</em> handled by the calling renderer itself,
+     * as required since Faces 5.0 (spec issue 1507).
+     * <p>
+     * A {@link ClientBehaviorHolder} exposes every HTML event name via {@link ClientBehaviorHolder#getEventNames()},
+     * not only those which happen to have a matching component property. So both
+     * <code>&lt;h:inputText oninput="..."/&gt;</code> and
+     * <code>&lt;h:inputText&gt;&lt;f:ajax event="input"/&gt;&lt;/h:inputText&gt;</code> have to end up as an
+     * <code>oninput</code> attribute, chained together when both are present. Each renderer only takes care of the
+     * events for which the component has a matching property, hence this generic pass for all the others.
+     * <p>
+     * This must be invoked <em>after</em> the renderer specific event attributes have been rendered, with
+     * <code>rendererHandledEvents</code> naming exactly the events the calling renderer renders itself
+     * (one of the <code>RENDERER_HANDLED_*</code> constants of this class).
+     *
+     * @param facesContext The involved faces context.
+     * @param writer The involved response writer.
+     * @param component The component being rendered.
+     * @param clientBehaviors The client behaviors of the component, may be <code>null</code> or empty.
+     * @param rendererHandledEvents The events the calling renderer renders itself, to be skipped here.
+     */
+    public static void renderAdditionalBehaviorEventHandlers(FacesContext facesContext, ResponseWriter writer,
+            UIComponent component, Map<String, List<ClientBehavior>> clientBehaviors,
+            Set<String> rendererHandledEvents) throws IOException
+    {
+        if (!(component instanceof ClientBehaviorHolder holder))
+        {
+            return;
+        }
+
+        Set<String> additionalEventNames =
+                collectAdditionalEventNames(component, clientBehaviors, rendererHandledEvents);
+        if (additionalEventNames == null)
+        {
+            return;
+        }
+
+        Map<String, Object> attributes = component.getAttributes();
+        Collection<String> eventNames = holder.getEventNames();
+
+        for (String eventName : additionalEventNames)
+        {
+            // as required by the standard HTML RenderKit, the name after "on" must be a supported event name
+            if (eventNames == null || !eventNames.contains(eventName))
+            {
+                continue;
+            }
+
+            String attributeName = CommonHtmlEvents.BEHAVIOR_EVENT_ATTRIBUTE_PREFIX + eventName;
+            Object attributeValue = attributes.get(attributeName);
+
+            renderBehaviorizedAttribute(facesContext, writer, attributeName, component, null, eventName, null,
+                    clientBehaviors, attributeName,
+                    attributeValue == null ? null : attributeValue.toString());
+        }
+    }
+
+    /**
+     * @return whether {@link #renderAdditionalBehaviorEventHandlers} would render anything for the given component.
+     *         Intended for renderers which have to decide upfront whether an element has to be started at all.
+     */
+    public static boolean hasAdditionalBehaviorEventHandlers(UIComponent component,
+            Map<String, List<ClientBehavior>> clientBehaviors, Set<String> rendererHandledEvents)
+    {
+        if (!(component instanceof ClientBehaviorHolder holder))
+        {
+            return false;
+        }
+
+        Set<String> additionalEventNames =
+                collectAdditionalEventNames(component, clientBehaviors, rendererHandledEvents);
+        if (additionalEventNames == null)
+        {
+            return false;
+        }
+
+        Collection<String> eventNames = holder.getEventNames();
+        if (eventNames == null)
+        {
+            return false;
+        }
+
+        for (String eventName : additionalEventNames)
+        {
+            if (eventNames.contains(eventName))
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Collects the names of all behavior events of the given component which the calling renderer does not handle
+     * itself, or <code>null</code> if there are none, which is the common case that has to stay cheap.
+     */
+    @SuppressWarnings("unchecked") // the marker entry is stored under an Object-valued component attribute
+    private static Set<String> collectAdditionalEventNames(UIComponent component,
+            Map<String, List<ClientBehavior>> clientBehaviors, Set<String> rendererHandledEvents)
+    {
+        Set<String> additionalEventNames = null;
+
+        // 1) <f:ajax event="input"> and friends; any behavior whose event is not rendered by the renderer itself.
+        // Usually empty or holds a single renderer handled event, so this loop is a no-op in practice.
+        if (clientBehaviors != null && !clientBehaviors.isEmpty())
+        {
+            for (String eventName : clientBehaviors.keySet())
+            {
+                if (!rendererHandledEvents.contains(eventName))
+                {
+                    additionalEventNames = add(additionalEventNames, eventName);
+                }
+            }
+        }
+
+        // 2) + 3) in a SINGLE pass over the component attribute map, which is the only part of this method that
+        // every rendered component has to pay for. That map is the plain state helper map (no property descriptor
+        // lookups, no ValueExpression evaluation) and normally holds no more than the oam.* markers. It gives us
+        //   - the literal on* attributes without matching property, e.g. oninput="...", which the facelets
+        //     tag handler stores as plain attributes
+        //   - the EVENT_ATTRIBUTES_MARKED entry naming the ValueExpression bound ones, e.g. oninput="#{...}",
+        //     as those are not part of the attribute map at all
+        for (Map.Entry<String, Object> attribute : component.getAttributes().entrySet())
+        {
+            String name = attribute.getKey();
+            if (CommonHtmlEvents.isBehaviorEventAttribute(name))
+            {
+                String eventName = CommonHtmlEvents.getEventName(name);
+                if (!rendererHandledEvents.contains(eventName))
+                {
+                    additionalEventNames = add(additionalEventNames, eventName);
+                }
+            }
+            else if (CommonHtmlEvents.EVENT_ATTRIBUTES_MARKED.equals(name))
+            {
+                for (String markedName : (Set<String>) attribute.getValue())
+                {
+                    String eventName = CommonHtmlEvents.getEventName(markedName);
+                    if (!rendererHandledEvents.contains(eventName))
+                    {
+                        additionalEventNames = add(additionalEventNames, eventName);
+                    }
+                }
+            }
+        }
+
+        return additionalEventNames;
+    }
+
+    /**
+     * Lazily creates the collector; a sorted set keeps the rendered attribute order deterministic.
+     */
+    private static Set<String> add(Set<String> eventNames, String eventName)
+    {
+        Set<String> result = eventNames == null ? new TreeSet<>() : eventNames;
+        result.add(eventName);
+        return result;
+    }
+
     public static boolean renderBehaviorizedAttribute(
             FacesContext facesContext, ResponseWriter writer,
             String componentProperty, UIComponent component,
diff --git a/impl/src/main/java/org/apache/myfaces/view/facelets/component/JsfElementRenderer.java b/impl/src/main/java/org/apache/myfaces/view/facelets/component/JsfElementRenderer.java
index 5649cc34c..f929bc25d 100644
--- a/impl/src/main/java/org/apache/myfaces/view/facelets/component/JsfElementRenderer.java
+++ b/impl/src/main/java/org/apache/myfaces/view/facelets/component/JsfElementRenderer.java
@@ -118,6 +118,8 @@ public void encodeBegin(FacesContext facesContext, UIComponent component)
                 ClientBehaviorEvents.LOAD, behaviors, HTML.ONLOAD_ATTR);
         HtmlRendererUtils.renderBehaviorizedAttribute(facesContext, writer, HTML.ONUNLOAD_ATTR, component,
                 ClientBehaviorEvents.UNLOAD, behaviors, HTML.ONUNLOAD_ATTR);
+        CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, component, behaviors,
+                CommonHtmlEventsUtil.RENDERER_HANDLED_PASSTHROUGH_ELEMENT_EVENTS);
         
     }
 
diff --git a/impl/src/main/java/org/apache/myfaces/view/facelets/tag/faces/core/AjaxHandler.java b/impl/src/main/java/org/apache/myfaces/view/facelets/tag/faces/core/AjaxHandler.java
index 8958365d3..49b8ba37d 100644
--- a/impl/src/main/java/org/apache/myfaces/view/facelets/tag/faces/core/AjaxHandler.java
+++ b/impl/src/main/java/org/apache/myfaces/view/facelets/tag/faces/core/AjaxHandler.java
@@ -29,6 +29,7 @@
 import jakarta.faces.component.UniqueIdVendor;
 import jakarta.faces.component.behavior.AjaxBehavior;
 import jakarta.faces.component.behavior.ClientBehaviorHolder;
+import jakarta.faces.component.html.HtmlEvents;
 import jakarta.faces.context.FacesContext;
 import jakarta.faces.event.AjaxBehaviorEvent;
 import jakarta.faces.event.AjaxBehaviorListener;
@@ -357,7 +358,11 @@ else if (!cvh.getEventNames().contains(eventName))
             else
             {
                 throw new TagAttributeException(event,
-                        "event it is not a valid eventName defined for this component");
+                        "'" + eventName + "' is not a valid eventName defined for this component."
+                        + " Supported event names are " + cvh.getEventNames() + "."
+                        + " In case you wish to add new ones, then you can specify them as space-separated value"
+                        + " of the context-param with name "
+                        + HtmlEvents.ADDITIONAL_HTML_EVENT_NAMES_PARAM_NAME);
             }
         }
 
diff --git a/impl/src/main/resources/META-INF/componentClass20.vm b/impl/src/main/resources/META-INF/componentClass20.vm
index 1911a35f2..cdabf120f 100644
--- a/impl/src/main/resources/META-INF/componentClass20.vm
+++ b/impl/src/main/resources/META-INF/componentClass20.vm
@@ -102,6 +102,15 @@ $innersource
 ## and csv implements
 #if ($component.implements)
 #if ($component.implements == "jakarta.faces.component.behavior.ClientBehaviorHolder")
+## Spec issue 1507: jsf:element mirrors the standard HTML components and must expose every HTML event name,
+## equivalent to the spec's jakarta.faces.component.html components. See api componentClass20.vm.
+#if (${utils.getClassFromFullClass($component.className)} == "JsfElement")
+    @Override
+    public java.util.Collection<String> getEventNames()
+    {
+        return jakarta.faces.component.html.HtmlEvents.getHtmlBodyElementEventNames(getFacesContext());
+    }
+#else
     static private final java.util.Collection<String> CLIENT_EVENTS_LIST =
         java.util.Collections.unmodifiableCollection(
             java.util.Arrays.asList(
@@ -120,6 +129,7 @@ $innersource
     {
         return CLIENT_EVENTS_LIST;
     }
+#end
 
     @Override
     public void addClientBehavior(String eventName, jakarta.faces.component.behavior.ClientBehavior behavior)
diff --git a/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlFormRendererTest.java b/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlFormRendererTest.java
index 193372706..b3e6be5b5 100644
--- a/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlFormRendererTest.java
+++ b/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlFormRendererTest.java
@@ -91,7 +91,7 @@ public void testHtmlPropertyPassTru() throws Exception
     @Test
     public void testHtmlPropertyPassTruNotRendered() throws Exception 
     { 
-        HtmlRenderedAttr[] attrs = HtmlCheckAttributesUtil.generateAttrsNotRenderedForReadOnly();
+        HtmlRenderedAttr[] attrs = HtmlCheckAttributesUtil.generateAttrsNotRenderedForReadOnlyButBehaviorEvents();
 
         try {
             HtmlCheckAttributesUtil.checkRenderedAttributes(
diff --git a/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlGridRendererTest.java b/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlGridRendererTest.java
index 7e632ed07..d00c639d7 100644
--- a/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlGridRendererTest.java
+++ b/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlGridRendererTest.java
@@ -125,7 +125,7 @@ public void testHtmlPropertyPassTru() throws Exception
     @Test
     public void testHtmlPropertyPassTruNotRendered() throws Exception 
     { 
-        HtmlRenderedAttr[] attrs = HtmlCheckAttributesUtil.generateAttrsNotRenderedForReadOnly();
+        HtmlRenderedAttr[] attrs = HtmlCheckAttributesUtil.generateAttrsNotRenderedForReadOnlyButBehaviorEvents();
 
         HtmlCheckAttributesUtil.checkRenderedAttributes(
                 panelGrid, facesContext, writer, attrs);
diff --git a/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlGroupRendererTest.java b/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlGroupRendererTest.java
index e1f79f1e5..d27ea90c1 100644
--- a/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlGroupRendererTest.java
+++ b/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlGroupRendererTest.java
@@ -117,7 +117,7 @@ public void testHtmlPropertyPassTru() throws Exception
     @Test
     public void testHtmlPropertyPassTruNotRendered() throws Exception
     { 
-        HtmlRenderedAttr[] attrs = HtmlCheckAttributesUtil.generateAttrsNotRenderedForReadOnly();        
+        HtmlRenderedAttr[] attrs = HtmlCheckAttributesUtil.generateAttrsNotRenderedForReadOnlyButBehaviorEvents();        
 
         HtmlCheckAttributesUtil.checkRenderedAttributes(
                 panelGroup, facesContext, writer, attrs);
diff --git a/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlImageRendererTest.java b/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlImageRendererTest.java
index c351390f6..8216210f9 100644
--- a/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlImageRendererTest.java
+++ b/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlImageRendererTest.java
@@ -105,7 +105,7 @@ public void testHtmlPropertyPassTru() throws Exception
     @Test
     public void testHtmlPropertyPassTruNotRendered() throws Exception
     { 
-        HtmlRenderedAttr[] attrs = HtmlCheckAttributesUtil.generateAttrsNotRenderedForReadOnly();
+        HtmlRenderedAttr[] attrs = HtmlCheckAttributesUtil.generateAttrsNotRenderedForReadOnlyButBehaviorEvents();
         
         graphicImage.setId("img1");
         graphicImage.setValue("http://www.apache.org/images/feather.gif");
diff --git a/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlTableRendererTest.java b/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlTableRendererTest.java
index fbc2ef9c5..13034f332 100644
--- a/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlTableRendererTest.java
+++ b/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlTableRendererTest.java
@@ -97,7 +97,7 @@ public void testHtmlPropertyPassTru() throws Exception
     @Test
     public void testHtmlPropertyPassTruNotRendered() throws Exception
     {
-        HtmlRenderedAttr[] attrs = HtmlCheckAttributesUtil.generateAttrsNotRenderedForReadOnly();
+        HtmlRenderedAttr[] attrs = HtmlCheckAttributesUtil.generateAttrsNotRenderedForReadOnlyButBehaviorEvents();
         
         HtmlCheckAttributesUtil.checkRenderedAttributes(
                 dataTable, facesContext, writer, attrs);
diff --git a/impl/src/test/java/org/apache/myfaces/renderkit/html/behavior/AdditionalHtmlEventsClientBehaviorRendererTest.java b/impl/src/test/java/org/apache/myfaces/renderkit/html/behavior/AdditionalHtmlEventsClientBehaviorRendererTest.java
new file mode 100644
index 000000000..8228000d3
--- /dev/null
+++ b/impl/src/test/java/org/apache/myfaces/renderkit/html/behavior/AdditionalHtmlEventsClientBehaviorRendererTest.java
@@ -0,0 +1,352 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.myfaces.renderkit.html.behavior;
+
+import jakarta.el.ValueExpression;
+import jakarta.faces.component.UIComponent;
+import jakarta.faces.component.behavior.AjaxBehavior;
+import jakarta.faces.component.behavior.ClientBehaviorHolder;
+import jakarta.faces.component.html.HtmlCommandButton;
+import jakarta.faces.component.html.HtmlEvents;
+import jakarta.faces.component.html.HtmlInputText;
+import jakarta.faces.component.html.HtmlOutputLink;
+import jakarta.faces.component.html.HtmlPanelGroup;
+import jakarta.faces.render.Renderer;
+
+import org.apache.myfaces.view.facelets.component.JsfElement;
+
+import org.apache.myfaces.test.el.MockValueExpression;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Spec issue 1507: every HTML event is a supported behavior event of the standard HTML components, even when the
+ * component has no matching property. So both <code>oninput="..."</code> and
+ * <code>&lt;f:ajax event="input"/&gt;</code> have to end up as an <code>oninput</code> attribute.
+ * <p>
+ * The inherited test cases cover exactly that for a set of events which have no property on any standard component,
+ * this class adds the cases which are specific to this feature.
+ */
+public class AdditionalHtmlEventsClientBehaviorRendererTest extends AbstractClientBehaviorTestCase
+{
+    private HtmlRenderedClientEventAttr[] attrs = null;
+
+    @Override
+    @BeforeEach
+    public void setUp() throws Exception
+    {
+        super.setUp();
+        attrs = new HtmlRenderedClientEventAttr[]
+        {
+            new HtmlRenderedClientEventAttr("oninput", "input"),
+            new HtmlRenderedClientEventAttr("onbeforeinput", "beforeinput"),
+            new HtmlRenderedClientEventAttr("oncontextmenu", "contextmenu"),
+            new HtmlRenderedClientEventAttr("oncopy", "copy"),
+            new HtmlRenderedClientEventAttr("onpaste", "paste"),
+            new HtmlRenderedClientEventAttr("ondrop", "drop"),
+            new HtmlRenderedClientEventAttr("onwheel", "wheel"),
+            new HtmlRenderedClientEventAttr("ontoggle", "toggle"),
+            new HtmlRenderedClientEventAttr("oninvalid", "invalid")
+        };
+    }
+
+    @Override
+    @AfterEach
+    public void tearDown() throws Exception
+    {
+        super.tearDown();
+        attrs = null;
+    }
+
+    @Override
+    protected UIComponent createComponentToTest()
+    {
+        return new HtmlInputText();
+    }
+
+    @Override
+    protected HtmlRenderedClientEventAttr[] getClientBehaviorHtmlRenderedAttributes()
+    {
+        return attrs;
+    }
+
+    /**
+     * A plain on* attribute without a matching property must be rendered as a pass through attribute.
+     */
+    @Test
+    public void testPlainAttributeWithoutBehaviorIsRendered() throws Exception
+    {
+        HtmlInputText input = new HtmlInputText();
+        input.getAttributes().put("oninput", "alert('input')");
+
+        input.encodeAll(facesContext);
+
+        Assertions.assertTrue(outputWriter.toString().contains(" oninput=\"alert('input')\""),
+                outputWriter.toString());
+    }
+
+    /**
+     * An on* attribute bound to a ValueExpression is neither a property nor part of the component attribute map key
+     * set, so it needs the {@code EVENT_ATTRIBUTES_MARKED} marker to be found at render time.
+     */
+    @Test
+    public void testValueExpressionAttributeWithoutBehaviorIsRendered() throws Exception
+    {
+        ValueExpression expression = new MockValueExpression("#{requestScope.oninputScript}", String.class);
+        expression.setValue(facesContext.getELContext(), "alert(1)");
+
+        HtmlInputText input = new HtmlInputText();
+        input.setValueExpression("oninput", expression);
+
+        input.encodeAll(facesContext);
+
+        Assertions.assertTrue(outputWriter.toString().contains(" oninput=\"alert(1)\""), outputWriter.toString());
+    }
+
+    /**
+     * An attribute which merely starts with "on" but is no known event must not be rendered, as required by the
+     * standard HTML RenderKit: the substring after "on" has to be contained in getEventNames().
+     */
+    @Test
+    public void testUnknownEventAttributeIsNotRendered() throws Exception
+    {
+        HtmlInputText input = new HtmlInputText();
+        input.getAttributes().put("onsomethingweird", "alert('nope')");
+
+        input.encodeAll(facesContext);
+
+        Assertions.assertFalse(outputWriter.toString().contains("onsomethingweird"), outputWriter.toString());
+    }
+
+    /**
+     * The very use case of the spec issue: an f:ajax on an event which no component declares as a property.
+     */
+    @Test
+    public void testAjaxBehaviorOnAdditionalEventIsRendered() throws Exception
+    {
+        HtmlInputText input = new HtmlInputText();
+        input.addClientBehavior("input", new AjaxBehavior());
+
+        input.encodeAll(facesContext);
+
+        String output = outputWriter.toString();
+        Assertions.assertTrue(output.contains(" oninput=\""), output);
+        Assertions.assertTrue(output.contains("myfaces.ab("), output);
+    }
+
+    /**
+     * The renderer specific attributes must keep working exactly as before, next to the additional ones.
+     */
+    @Test
+    public void testRendererSpecificAndAdditionalEventsAreCombined() throws Exception
+    {
+        HtmlInputText input = new HtmlInputText();
+        input.setOnclick("alert('click')");
+        input.getAttributes().put("oninput", "alert('input')");
+
+        input.encodeAll(facesContext);
+
+        String output = outputWriter.toString();
+        Assertions.assertTrue(output.contains(" onclick=\"alert('click')\""), output);
+        Assertions.assertTrue(output.contains(" oninput=\"alert('input')\""), output);
+    }
+
+    /**
+     * Not only inputs; the feature applies to every standard HTML component, which are spread over quite a few
+     * different renderers and rendering paths.
+     */
+    @Test
+    public void testAdditionalEventsOnOtherComponents() throws Exception
+    {
+        assertAdditionalEventRendered(new HtmlCommandButton());
+        assertAdditionalEventRendered(new HtmlOutputLink());
+
+        HtmlPanelGroup group = new HtmlPanelGroup();
+        group.setLayout("block");
+        assertAdditionalEventRendered(group);
+    }
+
+    /**
+     * An event which IS rendered by some renderers (here: focus by the field renderers) but NOT by the renderer at
+     * hand (h:panelGroup only renders the mouse/key/click events itself) must be rendered generically as well.
+     */
+    @Test
+    public void testFieldEventOnNonFieldComponent() throws Exception
+    {
+        HtmlPanelGroup group = new HtmlPanelGroup();
+        group.setLayout("block");
+        group.getAttributes().put("onfocus", "doFocus()");
+        ((ClientBehaviorHolder) group).addClientBehavior("blur", new AjaxBehavior());
+
+        group.encodeAll(facesContext);
+        String output = outputWriter.toString();
+
+        Assertions.assertTrue(output.contains(" onfocus=\"doFocus()\""), output);
+        Assertions.assertTrue(output.contains(" onblur=\""), output);
+    }
+
+    /**
+     * The opposite: an event which the renderer at hand DOES render itself must not be rendered a second time by
+     * the generic pass.
+     */
+    @Test
+    public void testRendererHandledEventIsNotRenderedTwice() throws Exception
+    {
+        HtmlInputText input = new HtmlInputText();
+        input.setOnclick("doClick()");
+        input.addClientBehavior("focus", new AjaxBehavior());
+
+        input.encodeAll(facesContext);
+        String output = outputWriter.toString();
+
+        Assertions.assertEquals(output.indexOf(" onclick=\""), output.lastIndexOf(" onclick=\""), output);
+        Assertions.assertEquals(output.indexOf(" onfocus=\""), output.lastIndexOf(" onfocus=\""), output);
+    }
+
+    private void assertAdditionalEventRendered(UIComponent component) throws Exception
+    {
+        component.getAttributes().put("oninput", "alert('input')");
+        ((ClientBehaviorHolder) component).addClientBehavior("wheel", new AjaxBehavior());
+
+        outputWriter.reset();
+        component.encodeAll(facesContext);
+        String output = outputWriter.toString();
+
+        Assertions.assertTrue(output.contains(" oninput=\"alert('input')\""),
+                component.getClass().getSimpleName() + ": " + output);
+        Assertions.assertTrue(output.contains(" onwheel=\""),
+                component.getClass().getSimpleName() + ": " + output);
+    }
+
+    /**
+     * jsf:element mirrors the standard HTML components and must expose and render every HTML event as well.
+     */
+    @Test
+    public void testAdditionalEventsOnJsfElement() throws Exception
+    {
+        JsfElement element = new JsfElement();
+        element.getPassThroughAttributes().put(Renderer.PASSTHROUGH_RENDERER_LOCALNAME_KEY, "div");
+        element.getAttributes().put("oninput", "doInput()");
+        element.addClientBehavior("wheel", new AjaxBehavior());
+
+        Assertions.assertTrue(element.getEventNames().contains("input"));
+        Assertions.assertTrue(element.getEventNames().contains("scrollend"));
+
+        element.encodeAll(facesContext);
+        String output = outputWriter.toString();
+
+        Assertions.assertTrue(output.contains(" oninput=\"doInput()\""), output);
+        Assertions.assertTrue(output.contains(" onwheel=\""), output);
+    }
+
+    /**
+     * The event names must reflect the component type, as the spec assigns a different set per component category.
+     */
+    @Test
+    public void testEventNamesPerComponentCategory()
+    {
+        // EditableValueHolder
+        Assertions.assertTrue(new HtmlInputText().getEventNames().contains("valueChange"));
+        Assertions.assertFalse(new HtmlInputText().getEventNames().contains("action"));
+
+        // ActionSource
+        Assertions.assertTrue(new HtmlCommandButton().getEventNames().contains("action"));
+        Assertions.assertFalse(new HtmlCommandButton().getEventNames().contains("valueChange"));
+
+        // neither of both
+        Assertions.assertFalse(new HtmlPanelGroup().getEventNames().contains("action"));
+        Assertions.assertFalse(new HtmlPanelGroup().getEventNames().contains("valueChange"));
+
+        // all of them expose the plain HTML events, including the window level ones
+        for (UIComponent component : new UIComponent[]
+                { new HtmlInputText(), new HtmlCommandButton(), new HtmlPanelGroup() })
+        {
+            ClientBehaviorHolder holder = (ClientBehaviorHolder) component;
+            Assertions.assertTrue(holder.getEventNames().contains("input"));
+            Assertions.assertTrue(holder.getEventNames().contains("click"));
+            Assertions.assertTrue(holder.getEventNames().contains("scrollend"));
+        }
+    }
+
+    /**
+     * The default event names as mandated by the spec.
+     */
+    @Test
+    public void testDefaultEventNames()
+    {
+        Assertions.assertEquals("valueChange", new HtmlInputText().getDefaultEventName());
+        Assertions.assertEquals("action", new HtmlCommandButton().getDefaultEventName());
+        Assertions.assertEquals("click", new HtmlOutputLink().getDefaultEventName());
+        Assertions.assertNull(new HtmlPanelGroup().getDefaultEventName());
+    }
+
+    /**
+     * Unknown event names can be added via context-param, and are then usable like any built in one.
+     */
+    @Test
+    public void testAdditionalEventNamesContextParam() throws Exception
+    {
+        servletContext.addInitParameter(HtmlEvents.ADDITIONAL_HTML_EVENT_NAMES_PARAM_NAME, "  foo\t bar\nfoo ");
+
+        Assertions.assertTrue(HtmlEvents.getAdditionalHtmlEventNames(facesContext).contains("foo"));
+        Assertions.assertTrue(HtmlEvents.getAdditionalHtmlEventNames(facesContext).contains("bar"));
+
+        HtmlInputText input = new HtmlInputText();
+        Assertions.assertTrue(input.getEventNames().contains("foo"));
+
+        input.getAttributes().put("onfoo", "alert('foo')");
+        input.addClientBehavior("bar", new AjaxBehavior());
+        input.encodeAll(facesContext);
+
+        String output = outputWriter.toString();
+        Assertions.assertTrue(output.contains(" onfoo=\"alert('foo')\""), output);
+        Assertions.assertTrue(output.contains(" onbar=\""), output);
+    }
+
+    /**
+     * Without the context-param there must be no stray empty event name, and the collections must be sorted,
+     * distinct and layered as documented.
+     */
+    @Test
+    public void testEventNameCollections()
+    {
+        Assertions.assertTrue(HtmlEvents.getAdditionalHtmlEventNames(facesContext).isEmpty());
+
+        Assertions.assertTrue(HtmlEvents.getHtmlBodyElementEventNames(facesContext)
+                .containsAll(HtmlEvents.getHtmlDocumentElementEventNames(facesContext)));
+        Assertions.assertTrue(HtmlEvents.getFacesActionSourceEventNames(facesContext)
+                .containsAll(HtmlEvents.getHtmlBodyElementEventNames(facesContext)));
+        Assertions.assertTrue(HtmlEvents.getFacesEditableValueHolderEventNames(facesContext)
+                .containsAll(HtmlEvents.getHtmlBodyElementEventNames(facesContext)));
+
+        // the window level events are exclusive to the body element event names
+        Assertions.assertFalse(HtmlEvents.getHtmlDocumentElementEventNames(facesContext).contains("load"));
+        Assertions.assertTrue(HtmlEvents.getHtmlBodyElementEventNames(facesContext).contains("load"));
+
+        String previous = null;
+        for (String eventName : HtmlEvents.getFacesEditableValueHolderEventNames(facesContext))
+        {
+            Assertions.assertTrue(previous == null || previous.compareTo(eventName) < 0,
+                    "not sorted and distinct at " + eventName);
+            previous = eventName;
+        }
+    }
+}
diff --git a/impl/src/test/java/org/apache/myfaces/renderkit/html/behavior/HtmlBodyClientBehaviorRendererTest.java b/impl/src/test/java/org/apache/myfaces/renderkit/html/behavior/HtmlBodyClientBehaviorRendererTest.java
index 14f4c8979..1b58f6a56 100644
--- a/impl/src/test/java/org/apache/myfaces/renderkit/html/behavior/HtmlBodyClientBehaviorRendererTest.java
+++ b/impl/src/test/java/org/apache/myfaces/renderkit/html/behavior/HtmlBodyClientBehaviorRendererTest.java
@@ -22,6 +22,7 @@
 import jakarta.faces.component.behavior.AjaxBehavior;
 import jakarta.faces.component.behavior.ClientBehaviorHolder;
 import jakarta.faces.component.html.HtmlBody;
+import jakarta.faces.component.html.HtmlEvents;
 
 import org.apache.myfaces.renderkit.ClientBehaviorEvents;
 import org.apache.myfaces.renderkit.html.util.HTML;
@@ -43,13 +44,11 @@ public class HtmlBodyClientBehaviorRendererTest extends AbstractClientBehaviorTe
     public void setUp() throws Exception
     {
         super.setUp();
-        attrs = (HtmlRenderedClientEventAttr[]) 
-            org.apache.myfaces.util.lang.ArrayUtils.concat( 
-                    HtmlClientEventAttributesUtil.generateClientBehaviorEventAttrs(),
-                new HtmlRenderedClientEventAttr[]{
-                    new HtmlRenderedClientEventAttr(HTML.ONLOAD_ATTR, ClientBehaviorEvents.LOAD),
-                    new HtmlRenderedClientEventAttr(HTML.ONUNLOAD_ATTR, ClientBehaviorEvents.UNLOAD)
-                });
+        // NOTE: "load" and "unload" are deliberately absent here. Since Faces 5.0 (spec issue 1507) a component
+        // representing the HTML <body> element exposes HtmlEvents#getHtmlDocumentElementEventNames(), which excludes
+        // the events that the <body> element forwards to the Window object instead of firing on the element itself.
+        // See testWindowEventsAreNoBehaviorEvents() and testWindowEventsCanBeReAddedViaContextParam() below.
+        attrs = HtmlClientEventAttributesUtil.generateClientBehaviorEventAttrs();
     }
 
     @Override
@@ -100,4 +99,61 @@ public void testClientBehaviorHolderRendersIdAndName()
         }
     }
 
+    /**
+     * Spec issue 1507: the events which the HTML &lt;body&gt; element forwards to the Window object must not be
+     * exposed as behavior events of the component, so &lt;f:ajax event="load"/&gt; is no longer supported on
+     * &lt;h:body&gt;. The plain onload attribute is unaffected, it is still a property of the component.
+     */
+    @Test
+    public void testWindowEventsAreNoBehaviorEvents()
+    {
+        HtmlBody body = new HtmlBody();
+
+        Assertions.assertFalse(body.getEventNames().contains(ClientBehaviorEvents.LOAD));
+        Assertions.assertFalse(body.getEventNames().contains(ClientBehaviorEvents.UNLOAD));
+        Assertions.assertTrue(body.getEventNames().contains(ClientBehaviorEvents.CLICK));
+
+        // still supported as a plain attribute
+        body.setOnload("alert('load')");
+        try
+        {
+            body.encodeAll(facesContext);
+            Assertions.assertTrue(outputWriter.toString().contains(HTML.ONLOAD_ATTR + "=\"alert('load')\""));
+        }
+        catch (Exception e)
+        {
+            Assertions.fail(e.getMessage());
+        }
+    }
+
+    /**
+     * Spec issue 1507: whatever HtmlEvents does not know about can be added via a context-param, which is also the
+     * migration path for applications relying on &lt;f:ajax event="load"/&gt; on &lt;h:body&gt;.
+     */
+    @Test
+    public void testWindowEventsCanBeReAddedViaContextParam()
+    {
+        servletContext.addInitParameter(HtmlEvents.ADDITIONAL_HTML_EVENT_NAMES_PARAM_NAME, "load unload");
+
+        HtmlBody body = new HtmlBody();
+
+        Assertions.assertTrue(body.getEventNames().contains(ClientBehaviorEvents.LOAD));
+        Assertions.assertTrue(body.getEventNames().contains(ClientBehaviorEvents.UNLOAD));
+
+        body.addClientBehavior(ClientBehaviorEvents.LOAD, new AjaxBehavior());
+        try
+        {
+            body.encodeAll(facesContext);
+            String output = outputWriter.toString();
+            Assertions.assertTrue(output.contains(HTML.ONLOAD_ATTR + "=\""), output);
+            // the body renderer renders onload itself; the generic pass must not render it a second time
+            Assertions.assertEquals(output.indexOf(HTML.ONLOAD_ATTR + "=\""),
+                    output.lastIndexOf(HTML.ONLOAD_ATTR + "=\""), output);
+        }
+        catch (Exception e)
+        {
+            Assertions.fail(e.getMessage());
+        }
+    }
+
 }
diff --git a/impl/src/test/java/org/apache/myfaces/test/utils/HtmlCheckAttributesUtil.java b/impl/src/test/java/org/apache/myfaces/test/utils/HtmlCheckAttributesUtil.java
index a91b339f8..2f3090d00 100644
--- a/impl/src/test/java/org/apache/myfaces/test/utils/HtmlCheckAttributesUtil.java
+++ b/impl/src/test/java/org/apache/myfaces/test/utils/HtmlCheckAttributesUtil.java
@@ -257,22 +257,44 @@ public static HtmlRenderedAttr[] generateBasicAttrs(boolean isOnSelectNeeded)
         return attrs;
     }
     
-    public static HtmlRenderedAttr[] generateAttrsNotRenderedForReadOnly() 
+    public static HtmlRenderedAttr[] generateAttrsNotRenderedForReadOnly()
     {
         HtmlRenderedAttr[] attrs = {
             //_AccesskeyProperty
             new HtmlRenderedAttr("accesskey", 0),
             //_FocusBlurProperties
-            new HtmlRenderedAttr("onfocus", 0), 
+            new HtmlRenderedAttr("onfocus", 0),
             new HtmlRenderedAttr("onblur", 0),
             //_ChangeSelectProperties
-            new HtmlRenderedAttr("onchange", 0), 
+            new HtmlRenderedAttr("onchange", 0),
             new HtmlRenderedAttr("onselect", 0),
             //_TabindexProperty
             new HtmlRenderedAttr("tabindex", 0)
         };
         return attrs;
     }
+
+    /**
+     * Since Faces 5.0 (spec issue 1507) behavior event attributes pass through generically on every
+     * ClientBehaviorHolder component, even without a matching component property. So on such components only the
+     * non-event attributes stay unrendered.
+     */
+    public static HtmlRenderedAttr[] generateAttrsNotRenderedForReadOnlyButBehaviorEvents()
+    {
+        HtmlRenderedAttr[] attrs = {
+            //_AccesskeyProperty
+            new HtmlRenderedAttr("accesskey", 0),
+            //_FocusBlurProperties
+            new HtmlRenderedAttr("onfocus"),
+            new HtmlRenderedAttr("onblur"),
+            //_ChangeSelectProperties
+            new HtmlRenderedAttr("onchange"),
+            new HtmlRenderedAttr("onselect"),
+            //_TabindexProperty
+            new HtmlRenderedAttr("tabindex", 0)
+        };
+        return attrs;
+    }
     
     public static HtmlRenderedAttr[] generateBasicReadOnlyAttrs() {
         HtmlRenderedAttr[] attrs = {
diff --git a/impl/src/test/java/org/apache/myfaces/view/facelets/tag/faces/html/AdditionalHtmlEventsTestCase.java b/impl/src/test/java/org/apache/myfaces/view/facelets/tag/faces/html/AdditionalHtmlEventsTestCase.java
new file mode 100644
index 000000000..bc82588ea
--- /dev/null
+++ b/impl/src/test/java/org/apache/myfaces/view/facelets/tag/faces/html/AdditionalHtmlEventsTestCase.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.myfaces.view.facelets.tag.faces.html;
+
+import java.io.StringWriter;
+
+import jakarta.faces.component.UIComponent;
+import jakarta.faces.component.UIInput;
+import jakarta.faces.component.UIOutput;
+import jakarta.faces.component.UIViewRoot;
+import jakarta.faces.component.behavior.AjaxBehavior;
+import jakarta.faces.component.behavior.ClientBehaviorHolder;
+import jakarta.faces.component.html.HtmlInputText;
+
+import org.apache.myfaces.renderkit.html.HtmlAjaxBehaviorRenderer;
+import org.apache.myfaces.renderkit.html.HtmlTextRenderer;
+import org.apache.myfaces.test.mock.MockResponseWriter;
+import org.apache.myfaces.view.facelets.AbstractFaceletTestCase;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Spec issue 1507, end to end: an on* attribute without a matching component property has to survive the facelet
+ * tag handling and end up in the rendered markup, on its own as well as chained with an f:ajax on the same event.
+ */
+public class AdditionalHtmlEventsTestCase extends AbstractFaceletTestCase
+{
+
+    @Override
+    protected void setupComponents() throws Exception
+    {
+        application.addComponent(UIViewRoot.COMPONENT_TYPE, UIViewRoot.class.getName());
+        application.addComponent(UIOutput.COMPONENT_TYPE, UIOutput.class.getName());
+        application.addComponent(HtmlInputText.COMPONENT_TYPE, HtmlInputText.class.getName());
+        application.addBehavior(AjaxBehavior.BEHAVIOR_ID, AjaxBehavior.class.getName());
+    }
+
+    @Override
+    protected void setupConvertersAndValidators() throws Exception
+    {
+    }
+
+    @Override
+    protected void setupRenderers() throws Exception
+    {
+        renderKit.addRenderer(UIInput.COMPONENT_FAMILY, "jakarta.faces.Text", new HtmlTextRenderer());
+        renderKit.addRenderer(UIOutput.COMPONENT_FAMILY, "jakarta.faces.Text", new HtmlTextRenderer());
+        renderKit.addClientBehaviorRenderer(AjaxBehavior.BEHAVIOR_ID, new HtmlAjaxBehaviorRenderer());
+    }
+
+    @Test
+    public void testAdditionalHtmlEvents() throws Exception
+    {
+        externalContext.getRequestMap().put("inputScript", "doEl()");
+
+        UIViewRoot root = facesContext.getViewRoot();
+        vdl.buildView(facesContext, root, "additionalHtmlEvents.xml");
+
+        // a literal on* attribute without matching property
+        String plain = encode(root, "plain");
+        Assertions.assertTrue(plain.contains("oninput=\"doPlain()\""), plain);
+
+        // the same, but bound to a ValueExpression
+        String el = encode(root, "el");
+        Assertions.assertTrue(el.contains("oninput=\"doEl()\""), el);
+
+        // <f:ajax event="input"> on a component which has no oninput property
+        Assertions.assertEquals("[input]",
+                ((ClientBehaviorHolder) root.findComponent("ajax")).getClientBehaviors().keySet().toString());
+        String ajax = encode(root, "ajax");
+        Assertions.assertTrue(ajax.contains("oninput=\""), ajax);
+        Assertions.assertTrue(ajax.contains("myfaces.ab("), ajax);
+
+        // both of them on the same component must be chained
+        String both = encode(root, "both");
+        Assertions.assertTrue(both.contains("faces.util.chain("), both);
+        Assertions.assertTrue(both.contains("doBoth()"), both);
+        Assertions.assertTrue(both.contains("myfaces.ab("), both);
+    }
+
+    private String encode(UIViewRoot root, String id) throws Exception
+    {
+        UIComponent component = root.findComponent(id);
+        Assertions.assertNotNull(component, "component " + id + " not found");
+
+        StringWriter sw = new StringWriter();
+        facesContext.setResponseWriter(new MockResponseWriter(sw));
+        component.encodeAll(facesContext);
+        sw.flush();
+        return sw.toString();
+    }
+}
diff --git a/impl/src/test/resources/org/apache/myfaces/view/facelets/tag/faces/html/additionalHtmlEvents.xml b/impl/src/test/resources/org/apache/myfaces/view/facelets/tag/faces/html/additionalHtmlEvents.xml
new file mode 100644
index 000000000..2d9e64118
--- /dev/null
+++ b/impl/src/test/resources/org/apache/myfaces/view/facelets/tag/faces/html/additionalHtmlEvents.xml
@@ -0,0 +1,36 @@
+<!--
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:ui="http://java.sun.com/jsf/facelets"
+      xmlns:h="http://java.sun.com/jsf/html"
+      xmlns:f="http://java.sun.com/jsf/core">
+<body>
+
+<ui:composition>
+    <h:inputText id="plain" oninput="doPlain()" />
+
+    <h:inputText id="el" oninput="#{requestScope.inputScript}" />
+
+    <h:inputText id="ajax">
+        <f:ajax event="input" />
+    </h:inputText>
+
+    <h:inputText id="both" oninput="doBoth()">
+        <f:ajax event="input" />
+    </h:inputText>
+</ui:composition>
+
+</body>
+</html>